fix(payment): persist amountPaid on payment confirmation (#20) #27
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/persist-amount-paid"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Issue #20:
confirmGuestPaymentandconfirmPaymentset order status to PROCESSING but never calledorder.setAmountPaid(...).amountPaidalways read null even after payment, blocking finance reconciliation.Changes
app.payment.letter-price(default 49) via@Valueand callorder.setAmountPaid(BigDecimal.valueOf(letterPrice))before save in bothconfirmGuestPaymentandconfirmPaymentamountPaid == 49after both confirm pathsTest plan
./gradlew :backend:test— BUILD SUCCESSFULCloses #20
confirmGuestPayment and confirmPayment set the order status to PROCESSING but never called order.setAmountPaid(...). As a result amountPaid always read null even after payment, blocking finance reconciliation against the Swish payout report. The frontend test mock (GuestPaymentRedirect.spec.ts) expected amountPaid: 49 but the real backend returned null. Changes: - OrderService: inject app.payment.letter-price (default 49) via @Value and set order.setAmountPaid(BigDecimal.valueOf(letterPrice)) before save in both confirmGuestPayment and confirmPayment - OrderServiceTest: assert amountPaid == 49 after both confirm paths Verified: ./gradlew :backend:test — BUILD SUCCESSFUL Closes #20Verdict: Approve-worthy fix —
amountPaidis now persisted on both payment-confirmation paths with state-guarded idempotency and matching tests. A few advisory nits; none block merge.Critical
None.
Warnings
amountPaidrecords the configured letter price, not the amount actually charged. Both confirm methods setamountPaid = BigDecimal.valueOf(letterPrice)fromapp.payment.letter-price. Fine for the Phase 0 honor-system flow, but the field name (amount_paid) and the issue's "finance reconciliation" goal imply "what the customer paid." Once Stripe/Swish webhook integration lands, prefer populating this from the payment intent's captured amount (handles partial payments, refunds, price drift mid-flight). At minimum, add a Javadoc note that this currently equals the expected price, not a verified received amount.Suggestions
@Valueconfig wiring is effectively untested.OrderServiceTestis a pure Mockito test (@InjectMocksdoes not resolve@Value), and the field initializer= 49is identical to the resolved default — so the test passes even ifapp.payment.letter-priceis misnamed, missing fromapplication.yml, or@Valueis deleted entirely. To actually exercise the config path: load the value via a@SpringBootTestslice and assert a non-sentinel price, orReflectionTestUtils.setField(orderService, "letterPrice", 99)and assert99, or at minimum drop the= 49initializer so a broken@Valuesurfaces as0.intmoney type.@Value("${app.payment.letter-price:49}") private int letterPrice = 49;declares the49default twice and the same value also sits inapplication.yml. Considerprivate BigDecimal letterPrice;(Spring converts the string to BigDecimal) andorder.setAmountPaid(letterPrice)directly — removes theBigDecimal.valueOf(int)cast, the redundant default, and future-proofs for öre (e.g. 49.50) since the column is alreadyprecision=10, scale=2.confirmGuestPayment(lines ~78-82) andconfirmPayment(lines ~100-104) run an identicalsetStatus(PROCESSING) -> setAmountPaid -> save -> notify -> returnsequence; extractprivate Order markPaidAndProcessing(Order order)(duplication pre-existed, deepened here).assertEquals(new BigDecimal("49"), result.getAmountPaid())passes here only because the mocked repo returns the same in-memory instance (scale 0). An integration test reloading from thescale=2column would yield49.00, andnew BigDecimal("49").equals(49.00)isfalse. PreferassertEquals(0, new BigDecimal("49").compareTo(result.getAmountPaid())), or assertnew BigDecimal("49.00").Looks Good
PENDING_PAYMENT(viarequirePendingOwnedBy/getOrderByGuestToken+ status check), so a duplicate confirmation throwsInvalidOrderStateExceptioninstead of double-recording.userId/guestTokenauthorization checks are untouched.amountPaidassertion;BigDecimalimport added in both source and test.💡 Redundant
= 49initializer +intmoney type. The49default lives in three places (the:49, the field init, andapplication.yml), and the= 49masks@Valuefailures in the unit test (Mockito's@InjectMocksnever resolves@Value, so the test rides on the initializer). Considerprivate BigDecimal letterPrice;andorder.setAmountPaid(letterPrice)directly — Spring converts the string to BigDecimal and you're future-proofed for öre (the column is alreadyscale=2). See review body for the full test-coverage note.♻️ The
setStatus -> setAmountPaid -> save -> notify -> returnblock is duplicated inconfirmPayment(line 102). Extractprivate Order markPaidAndProcessing(Order order)to collapse it (the duplication pre-existed but is deepened here).⚠️
amountPaidhere is the configuredletterPrice, not an amount verified as received from a payment provider. Fine for Phase 0 honor-system, but once Stripe/Swish webhooks land this should come from the captured payment-intent amount (handles partial payments / refunds / price drift). Worth a Javadoc note documenting the current assumption for finance.🧪 This assertion passes via the
= 49field initializer, not via@Value:@InjectMocksdoesn't resolve Spring@Value, so this test cannot catch a misnamedapp.payment.letter-pricePropertySource or a removed@Value. The guest-path twin at line 350 has the same gap. Strengthen withReflectionTestUtils.setField(orderService, "letterPrice", 99)+ assert99, or a@SpringBootTestslice that asserts the resolved config differs from a sentinel.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.