Skip to content

feat(payments): end-to-end Redsys hosted-redirect for Spain - #365

Merged
telivity-otaip merged 18 commits into
mainfrom
cursor/redsys-payment-e2e-b873
Sep 10, 2026
Merged

telivity-otaip merged 18 commits into
mainfrom
cursor/redsys-payment-e2e-b873

Conversation

@telivity-otaip

@telivity-otaip telivity-otaip commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Completes the Redsys (Spanish TPV) hosted-redirect payment path end-to-end.

Status

Approved by Agus after re-review. Head 2bc2c61 matches his independently reviewed tip (faaa96f). EUR sandbox auth/cancel + duplicate signed-callback replay verified; CI green (5/5).

Remediation (on PR head)

  1. Atomic verified finalization — Pending checkout creates no deposit; signature-verified success records the deposit and applies configured confirmation once.
  2. Server-authoritative return recovery — Full-page hosted return restores from payment state without React memory/storage.
  3. URL-length-safe return relay — Compact capability-bound API relay keeps provider return URLs within the 250-character limit while preserving the validated hotel page.
  4. Credentials & lifecycle hardening — Fail closed without credentials; correct currency minor units; valid void amounts; signed operation-bound REST results; encrypt/mask per-property secrets; PostgreSQL regressions in CI.

Deploy before payment traffic

Schedule before merge/enablement:

  1. Provision the credential-encryption key ring
  2. Run the idempotent Redsys credential backfill
  3. Apply migrations 0024–0026

Out of scope

Embedded inSite checkout — tracked in #366.

Test plan

  • EUR sandbox: pending / success / cancel / fresh-browser recovery / dual signed-callback replay
  • Tracked suite: 2,392 tests / 280 files (Agus)
  • CI green on 2bc2c61
Open in Web Open in Cursor 

cursoragent and others added 5 commits September 9, 2026 17:24
Wire booking deposit and folio authorize to submitRedirectNextAction when
PAYMENT_GATEWAY=redsys, and add a credentials form on the Redsys
integrations row (FUC, terminal, secret, environment).

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Shared client helper that auto-submits the PSP POST form returned as
nextAction from authorize/book.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Implement HMAC signing, authorize redirect nextAction, REST capture/void/refund,
per-property FUC credential resolution, signed MerchantURL webhook updates, and
client-config exposure so Spain desk and booking flows can collect card money.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Pass redirect return URLs through booking-engine authorize, skip auto-confirm
while checkout is pending, merge/mask Redsys secrets on Integrations upsert, and
keep raw secrets available to the credentials resolver.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Persist booking confirmation across hosted checkout, surface Folio return
toasts, and ship demo/docs/env examples for sandbox go-live.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
@telivity-otaip
telivity-otaip marked this pull request as ready for review September 9, 2026 17:34
telivity-otaip and others added 5 commits September 9, 2026 12:37
Dashboard ESLint does not define react-hooks/exhaustive-deps, so the
disable comment failed CI Lint & Type Check with a hard error.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
PaymentService now injects ConfigService and RedsysCredentialsService;
update Nest TestingModules and the legacy constructor seam so apps/api
vitest can resolve the service again.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Constructor gained runtimeConfig for Redsys gateway resolution; unit and
postgres pricing-lock helpers still used the old arity and shifted mocks.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
CI reported 2258 passed tests across 272 files; published counts were still 2249/269.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

@agustinjch agustinjch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I exercised the booking flow against the real Redsys sandbox and am requesting changes for three reproduced correctness failures:

  1. Booking return cannot recover the checkout. The generated OK/KO URLs return to origin-root routes, while the booking app uses an in-memory router and loses its state on the full-page round trip. A pathname-only correction is insufficient: the return must bootstrap from a durable, opaque booking/payment reference.
  2. A deposit is recorded before authorization. recordDeposit() runs as soon as Redsys returns a hosted-checkout action, while the payment is still pending. Cancelling or abandoning checkout leaves an actionable deposit ledger entry for money that was never authorized.
  3. Verified success does not complete auto-confirmation. Booking creation skips confirmation while redirect is pending, but the signed callback updates only the payment. With auto-confirm enabled, the payment becomes authorized while the reservation remains pending.

The browser return must be treated only as navigation. A signature-verified provider result must be the authority for payment state, deposit creation, and reservation confirmation, with an idempotent finalizer for duplicate/out-of-order callback and return delivery.

Non-blocking follow-up: Redsys inSite could provide a smoother embedded checkout, but it is a different REST/3DS integration and does not remove the need for the authoritative lifecycle fix above. I recommend evaluating it separately after this hosted-redirect path is made correct.

cursoragent and others added 2 commits September 9, 2026 23:08
Browser return is navigation only. Skip deposit ledger writes on hosted
redirect authorize, recover MemoryRouter state via haip_checkout token,
and run an idempotent finalizer from the MerchantURL webhook for payment
authorization, deposit creation, and booking-engine auto-confirm.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
@agustinjch

agustinjch commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

The requested remediation is implemented and independently reviewed. The four reviewed commits have now been applied directly to this PR, ending at 2bc2c614fc5b34b700a3cb28f8e44958b68cf657.

The superseded non-atomic finalizer was reverted before applying them, so the PR contains one finalization path. The current PR tree is byte-for-byte identical to the independently reviewed tree. The temporary helper PR #367 is therefore obsolete and has been closed.

Commits to bring onto this PR head, in order:

  • 5c80e9e — finalize verified redirect booking authorization atomically; pending checkout creates no deposit, verified success records the deposit and applies configured confirmation exactly once.
  • c452650 — recover a full-page hosted return from minimal server-authoritative payment state without React memory/storage.
  • 577384b — use a compact capability-bound API relay so provider return URLs remain within the 250-character limit while preserving the exact validated hotel page.
  • 2bc2c61 — fail closed without credentials; correct currency minor units; send valid void amounts; require signed operation-bound REST results; encrypt/mask per-property secrets; run PostgreSQL regressions in CI; refresh dashboard coverage and test counts.

Real EUR sandbox verification on the first three commits covered authorization and provider cancellation:

  • Before provider completion: payment pending, reservation pending, zero deposits.
  • Verified success: payment authorized, one EUR 1.00 held deposit, reservation confirmed.
  • Provider cancellation: payment failed, zero deposits, reservation remained pending.
  • A fresh browser recovered the success screen without prior booking state.
  • Two exact replays of the original signed success callback returned 200 and changed no payment, deposit, reservation, or audit rows.

After the final security/lifecycle commit, the tracked-tree run passed 2,392 tests across 280 files, including the PostgreSQL callback and encrypted-credential suites. An independent final review found all blocking findings addressed and no new breakage. Build and typecheck pass; lint has zero errors (existing warnings remain).

Deployment prerequisite: provision the documented credential-encryption key ring and run the idempotent Redsys credential backfill before enabling payment traffic. Migrations 0024–0026 must also be applied before the API/widget rollout.

The embedded inSite checkout remains deliberately separate and non-blocking: #366 tracks merchant activation, multi-tenant domain registration/CSP, iframe choice, idOper lifecycle, embedded EMV 3-D Secure, accessibility, fallback, and reuse of the same authoritative finalizer.

@agustinjch agustinjch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the four remediation commits were applied directly. The current head tree exactly matches the independently reviewed implementation. Real EUR sandbox authorization/cancellation and duplicate signed-callback replay passed; the tracked suite reports 2,392 passing tests, and all current GitHub checks are green. Ready to merge after the documented deployment prerequisite is scheduled: provision the credential-encryption key ring, run the idempotent Redsys credential backfill, and apply migrations 0024–0026 before payment traffic.

@telivity-otaip

Copy link
Copy Markdown
Collaborator Author

Gracias hermano.

@telivity-otaip
telivity-otaip merged commit 196bc7c into main Sep 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants