|
4 | 4 | NotFoundException, |
5 | 5 | } from '@nestjs/common'; |
6 | 6 | import { eq, and } from 'drizzle-orm'; |
| 7 | +import Decimal from 'decimal.js'; |
7 | 8 | import { folios, reservations, payments } from '@haip/database'; |
8 | 9 | import { DRIZZLE } from '../../database/database.module'; |
9 | 10 | import { FolioService } from './folio.service'; |
@@ -77,53 +78,70 @@ export class FolioRoutingService { |
77 | 78 | dto: TransferCityLedgerDto, |
78 | 79 | ) { |
79 | 80 | const sourceFolio = await this.folioService.findById(folioId, propertyId); |
80 | | - const remainingBalance = parseFloat(sourceFolio.balance); |
| 81 | + // Monetary compare on string representation via decimal.js (numeric-as-string). |
| 82 | + const remainingBalance = new Decimal(sourceFolio.balance); |
81 | 83 |
|
82 | | - if (remainingBalance <= 0) { |
| 84 | + if (remainingBalance.lte(0)) { |
83 | 85 | return { message: 'No outstanding balance to transfer' }; |
84 | 86 | } |
85 | 87 |
|
86 | | - // Create city ledger folio |
87 | | - const cityLedgerFolio = await this.folioService.create({ |
88 | | - propertyId, |
89 | | - guestId: sourceFolio.guestId, |
90 | | - type: 'city_ledger', |
91 | | - currencyCode: sourceFolio.currencyCode, |
92 | | - companyName: dto.companyName, |
93 | | - billingAddress: dto.billingAddress, |
94 | | - paymentTermsDays: dto.paymentTermsDays, |
95 | | - }); |
| 88 | + const amountStr = remainingBalance.toFixed(2); |
| 89 | + |
| 90 | + // Bug 3: wrap all mutating steps in a single transaction so partial failure |
| 91 | + // (e.g. CL folio created but payment insert fails) is impossible. |
| 92 | + // Idempotency-by-transferId is out of scope for this PR. |
| 93 | + const { cityLedgerFolio } = await this.db.transaction(async (tx: any) => { |
| 94 | + // Create city ledger folio |
| 95 | + const cityLedgerFolio = await this.folioService.create( |
| 96 | + { |
| 97 | + propertyId, |
| 98 | + guestId: sourceFolio.guestId, |
| 99 | + type: 'city_ledger', |
| 100 | + currencyCode: sourceFolio.currencyCode, |
| 101 | + companyName: dto.companyName, |
| 102 | + billingAddress: dto.billingAddress, |
| 103 | + paymentTermsDays: dto.paymentTermsDays, |
| 104 | + }, |
| 105 | + tx, |
| 106 | + ); |
| 107 | + |
| 108 | + // Record city_ledger payment on the source folio (zeroes out the guest folio) |
| 109 | + await tx |
| 110 | + .insert(payments) |
| 111 | + .values({ |
| 112 | + folioId, |
| 113 | + propertyId, |
| 114 | + method: 'city_ledger', |
| 115 | + amount: amountStr, |
| 116 | + currencyCode: sourceFolio.currencyCode, |
| 117 | + status: 'captured', |
| 118 | + processedAt: new Date(), |
| 119 | + notes: `Transferred to city ledger: ${dto.companyName}`, |
| 120 | + }); |
| 121 | + |
| 122 | + await this.folioService.recalculateBalance(folioId, propertyId, tx); |
| 123 | + |
| 124 | + // Post matching charge on the city ledger folio |
| 125 | + await this.folioService.postCharge( |
| 126 | + cityLedgerFolio.id, |
| 127 | + { |
| 128 | + propertyId, |
| 129 | + type: 'fee', |
| 130 | + description: `Transfer from folio ${sourceFolio.folioNumber}`, |
| 131 | + amount: amountStr, |
| 132 | + currencyCode: sourceFolio.currencyCode, |
| 133 | + serviceDate: new Date().toISOString(), |
| 134 | + }, |
| 135 | + tx, |
| 136 | + ); |
96 | 137 |
|
97 | | - // Record city_ledger payment on the source folio (zeroes out the guest folio) |
98 | | - await this.db |
99 | | - .insert(payments) |
100 | | - .values({ |
101 | | - folioId, |
102 | | - propertyId, |
103 | | - method: 'city_ledger', |
104 | | - amount: remainingBalance.toFixed(2), |
105 | | - currencyCode: sourceFolio.currencyCode, |
106 | | - status: 'captured', |
107 | | - processedAt: new Date(), |
108 | | - notes: `Transferred to city ledger: ${dto.companyName}`, |
109 | | - }); |
110 | | - |
111 | | - await this.folioService.recalculateBalance(folioId, propertyId); |
112 | | - |
113 | | - // Post matching charge on the city ledger folio |
114 | | - await this.folioService.postCharge(cityLedgerFolio.id, { |
115 | | - propertyId, |
116 | | - type: 'fee', |
117 | | - description: `Transfer from folio ${sourceFolio.folioNumber}`, |
118 | | - amount: remainingBalance.toFixed(2), |
119 | | - currencyCode: sourceFolio.currencyCode, |
120 | | - serviceDate: new Date().toISOString(), |
| 138 | + return { cityLedgerFolio }; |
121 | 139 | }); |
122 | 140 |
|
123 | 141 | return { |
124 | 142 | sourceFolioId: folioId, |
125 | 143 | cityLedgerFolioId: cityLedgerFolio.id, |
126 | | - transferredAmount: remainingBalance.toFixed(2), |
| 144 | + transferredAmount: amountStr, |
127 | 145 | }; |
128 | 146 | } |
129 | 147 | } |
0 commit comments