import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../../infrastructure/database/database.service';
import { CashService } from '../application/cash.service';
import { DomainConflictError } from '../../../common/errors/domain-conflict.error';
@Injectable()
export class CashPaymentIntegrationService {
constructor(
private readonly database: DatabaseService,
private readonly cash: CashService,
) {}
async onPaymentCaptured(input: {
organizationId: string;
locationId: string;
paymentId: string;
userId?: string;
}) {
const payments = await this.database.query<any>(
`select p.*, t.tender_type
from payments p
join tenders t on t.id=p.tender_id
where p.id=$1
and p.organization_id=$2
and p.location_id=$3
and p.status='captured'`,
[input.paymentId, input.organizationId, input.locationId],
);
const payment = payments[0];
if (!payment) throw new DomainConflictError('cash.payment_not_found');
if (payment.tender_type !== 'cash') return { ignored: true };
const shifts = await this.database.query<{ id: string }>(
`select id
from cash_shifts
where organization_id=$1
and location_id=$2
and status='open'
order by opened_at desc
limit 1`,
[input.organizationId, input.locationId],
);
if (!shifts[0]) throw new DomainConflictError('cash.no_open_shift');
return this.cash.recordCashSale({
organizationId: input.organizationId,
locationId: input.locationId,
cashShiftId: shifts[0].id,
paymentId: input.paymentId,
amountMinor:Number.parseInt(String(payment.amount_minor),10),
currencyCode: payment.currency_code,
userId: input.userId,
});
}
async onRefundCompleted(input: {
organizationId: string;
locationId: string;
refundId: string;
userId?: string;
}) {
const refunds = await this.database.query<any>(
`select r.*, p.tender_id, t.tender_type
from refunds r
join payments p on p.id=r.payment_id
join tenders t on t.id=p.tender_id
where r.id=$1
and r.organization_id=$2
and r.location_id=$3
and r.status='completed'`,
[input.refundId, input.organizationId, input.locationId],
);
const refund = refunds[0];
if (!refund) throw new DomainConflictError('cash.refund_not_found');
if (refund.tender_type !== 'cash') return { ignored: true };
const shifts = await this.database.query<{ id: string }>(
`select id
from cash_shifts
where organization_id=$1
and location_id=$2
and status='open'
order by opened_at desc
limit 1`,
[input.organizationId, input.locationId],
);
if (!shifts[0]) throw new DomainConflictError('cash.no_open_shift');
const existing = await this.database.query<{ id: string }>(
`select id from cash_movements
where organization_id=$3 and location_id=$4 and cash_shift_id=$1
and source_type='refund'
and source_id=$2
limit 1`,
[shifts[0].id, input.refundId, input.organizationId, input.locationId],
);
if (existing[0]) return { id: existing[0].id, replay: true };
const idRows = await this.database.query<{ id: string }>(
`insert into cash_movements(
id, organization_id, location_id, cash_shift_id,
movement_type, amount_minor, currency_code,
source_type, source_id, created_by
) values(gen_random_uuid(),$1,$2,$3,'cash_refund',$4,$5,'refund',$6,$7)
returning id`,
[
input.organizationId,
input.locationId,
shifts[0].id,
refund.amount_minor,
refund.currency_code,
input.refundId,
input.userId ?? null,
],
);
await this.database.query(
`update cash_shifts
set expected_cash_minor=greatest(0,expected_cash_minor-$1),
version=version+1
where id=$2 and organization_id=$3 and location_id=$4`,
[refund.amount_minor, shifts[0].id, input.organizationId, input.locationId],
);
return { id: idRows[0].id, replay: false };
}
}
