Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,28 @@ describe('BankTxConsumer', () => {
expect(cents(legs)).toBe(0);
});

// prod reality: a return bank_tx is linked from buy_crypto.chargebackBankTx — its buyCrypto (inbound-payment
// inverse) is null. The owed anchor must come from buyCryptoChargeback (bank_tx 206858/206864 regression).
it('books BUY_CRYPTO_RETURN linked only via buyCryptoChargeback (buyCrypto is null on returns)', async () => {
const buyCryptoChargeback = { id: 128422, amountInChf: 9226.11, totalFeeAmountChf: 0 } as any;
mockBatch([
bankTx({
type: BankTxType.BUY_CRYPTO_RETURN,
creditDebitIndicator: BankTxIndicator.DEBIT,
accountIban: 'CHF-IBAN',
amount: 9226.11,
buyCrypto: undefined,
buyCryptoChargeback,
}),
]);
await consumer.process();

const legs = booked[0].legs;
const owed = legs.find((l) => l.account.name === 'LIABILITY/buyCrypto-owed');
expect(owed.amountChf).toBe(9226.11); // completion CHF from the chargeback relation, no throw
expect(cents(legs)).toBe(0);
});

it('books KRAKEN DBIT as Dr TRANSIT/bank↔Kraken / Cr ASSET/bank (CHF, route nets to 0)', async () => {
mockBatch([
bankTx({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class BankTxConsumer {
SOURCE_TYPE,
afterForward,
this.bankTxRepo,
{ buyCrypto: true },
{ buyCrypto: true, buyCryptoChargeback: true },
async (tx: BankTx) => {
const t = tx.bookingDate ?? tx.created;
await this.reconcileBooking(
Expand All @@ -104,7 +104,7 @@ export class BankTxConsumer {
// DBIT only after 5 min (analog assignTransactions); settlement = bookingDate ?? created (§4.2)
const batch = await this.bankTxRepo.find({
where: { id: MoreThan(watermark.lastProcessedId), created: LessThan(Util.minutesBefore(5)) },
relations: { buyCrypto: true },
relations: { buyCrypto: true, buyCryptoChargeback: true },
order: { id: 'ASC' },
take: Config.ledger.backfillBatchSize,
});
Expand Down Expand Up @@ -283,12 +283,15 @@ export class BankTxConsumer {
// the CHF the buyCrypto-owed was opened with: §4.6 completion (amountInChf − totalFeeAmountChf), or — for a
// cutover-straddling buy_crypto whose owed was opened by the cutover (§6.1 per-row marker) — the opening CHF.
private async buyCryptoOwedChf(tx: BankTx): Promise<number> {
const openingChf = await this.cutoverOwedOpeningChf(tx.buyCrypto?.id);
// a return bank_tx is linked from buy_crypto.chargebackBankTx — tx.buyCrypto is the inbound-payment
// inverse and is null on returns
const buyCrypto = tx.buyCryptoChargeback ?? tx.buyCrypto;
const openingChf = await this.cutoverOwedOpeningChf(buyCrypto?.id);
if (openingChf != null) return openingChf; // cutover-straddling: debit the exact opening CHF anchor

const amountInChf = tx.buyCrypto?.amountInChf;
const amountInChf = buyCrypto?.amountInChf;
if (amountInChf == null) throw new Error(`bank_tx ${tx.id} BUY_CRYPTO_RETURN without buyCrypto.amountInChf`);
return Util.round(amountInChf - (tx.buyCrypto?.totalFeeAmountChf ?? 0), 2); // completion CHF (additive null-strategy)
return Util.round(amountInChf - (buyCrypto?.totalFeeAmountChf ?? 0), 2); // completion CHF (additive null-strategy)
}

// looks up the cutover per-row owed-opening leg CHF (§6.1 marker `${snapshotLogId}:buy_crypto-owed:${id}`); the
Expand Down