Repository navigation
Conversation
Omise / Opn Payments adapter scoped to the PromptPay push rail: a THB charge
(POST /charges, source[type]=promptpay) returns pending + a promptpay_qr
ChargeResult.Action; settlement arrives via an unsigned charge.complete webhook
that ParseWebhook normalizes (paid/payment_failed/ignored) but fails closed
rather than pretend to authenticate.
Modeled PSP-major (adapters/omise, PromptPay as a push chargeMode) per the
one-directory-per-PSP rule — PromptPay is a rail Omise fronts, not a PSP.
Contract & capabilities:
- capability.json: chargeModes [push] + currencies {thb: min 2000 satang}.
The ฿20 minimum is sourced from Omise's own invalid_charge error and verified
against the live test API; no max is documented, so none is declared.
- Business decline (failed/expired/reversed) -> ChargeResult{Status:"failed"}
nil error; transport failure (non-2xx, timeout, connection drop) -> a returned
error, so the scheduler lease-retries instead of consuming a dunning attempt.
- Webhook paid events set paymentRef/amount/currency together or refuse to emit.
Idempotency (verified live, documented honestly):
- opts.IdempotencyKey is sent verbatim as Omise's Idempotency-Key header. The
observed test account (API 2019-05-29) did NOT dedup on it. Because PromptPay
is push this is not a double-collect — Charge only mints a QR, so a retry
yields a second UNPAID QR that expires; reconcile by the paid webhook's charge
id. The key is still sent so dedup engages on versions that honor it.
Testing:
- certify battery green (CHG-01/03/04, PSH-01, WHK-PAID/IGNORED + derived
CUR-REJECT/WHK-CLOSED/CAT-NOOP laws), driven offline via WithBaseURL.
- gateway + webhook vectors as the language-neutral behavior contract; a future
TS twin must replay the same files.
- robustness_test.go covers what the declarative harness cannot express:
transport failure / connection drop, verbatim-key on retry, malformed webhook
bodies. 37 subtests, make check green.
- Charge wire shape, the THB minimum, and the idempotency behavior all verified
against the live Omise test API (2026-07-09); env-gated live_test.go pins the
happy-path run.
Known follow-ups (tracked in the adapter README): webhook wire shape live
verification, a GET /charges/{id} confirm-by-lookup helper, and an optional
@monetakit/omise TS twin.
There was a problem hiding this comment.
Pull request overview
Adds a new contrib-tier Go adapter at adapters/omise for Omise / Opn Payments, scoped to Thailand’s PromptPay push rail, including charge creation (QR issuance) and webhook normalization, plus language-neutral vector fixtures and certification replay.
Changes:
- Introduces
Chargefor PromptPay (POST /chargesform-encoded, Basic auth) returningpending+promptpay_qraction; maps terminal charge states to succeeded/failed vs. transport errors. - Adds
ParseWebhooknormalization forcharge.completeevents (unsigned webhooks; fail-closed if signature/secret is supplied). - Adds capability declaration + drift test, vector fixtures (
gateway.json,webhooks.json,certify.json), and conformance/robustness/live-sandbox tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| CHANGELOG.md | Announces the new Omise adapter in release notes. |
| adapters/omise/omise.go | Core adapter implementation: capabilities, HTTP plumbing, Charge/Vault, catalog no-ops. |
| adapters/omise/webhook.go | Webhook normalization for charge.complete into neutral WebhookEvent. |
| adapters/omise/webhook_test.go | Replays webhook vectors and asserts fail-closed behavior when signature/secret provided. |
| adapters/omise/vectors/webhooks.json | Language-neutral webhook normalization vectors. |
| adapters/omise/vectors/gateway.json | Language-neutral gateway/charge classification + request-shape vectors. |
| adapters/omise/vectors/certify.json | Fixtures for adapterkit/certify scenario replay. |
| adapters/omise/robustness_test.go | Go-only tests for transport failures, idempotency header passthrough, malformed webhook bodies. |
| adapters/omise/README.md | Adapter docs: scope, flow, idempotency caveat, unsigned webhook warning, live test instructions. |
| adapters/omise/MAINTAINERS | Declares maintainer handle for the adapter. |
| adapters/omise/live_test.go | Opt-in live sandbox probe against Omise test API. |
| adapters/omise/charge_test.go | Replays gateway vectors and validates request shape/basic auth/idempotency header. |
| adapters/omise/certify_test.go | Runs shared certification battery against vectors/certify.json. |
| adapters/omise/capability.json | Declares adapter capabilities (push-only, THB min). |
| adapters/omise/capability_test.go | Drift test ensuring Capabilities() matches capability.json. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+19
to
+20
| battery replay offline. Live-sandbox run and PromptPay charge-bound | ||
| declaration still TODO (see the adapter README). |
Comment on lines
+192
to
+196
| if status < 200 || status >= 300 { | ||
| var e omiseError | ||
| _ = json.Unmarshal(raw, &e) | ||
| return adapterkit.ChargeResult{}, fmt.Errorf("omise: POST /charges -> %d %s: %s", status, e.Code, e.Message) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First contrib PSP adapter for the Thailand beachhead: Omise / Opn Payments, scoped to its PromptPay push rail. PromptPay is a rail Omise fronts (not a PSP), so this is modeled PSP-major (
adapters/omise, PromptPay as apushchargeMode) per the one-directory-per-PSP rule — a future Thai acquirer for the same rail would be its own adapter dir.What's here
Charge—POST /charges(source[type]=promptpay, form-encoded, Basic auth) →pending+promptpay_qrChargeResult.Action. Business decline (failed/expired/reversed) →ChargeResult{Status:"failed"}nil error; transport failure (non-2xx / timeout / connection drop) → a returned error, so the scheduler lease-retries instead of consuming a dunning attempt.ParseWebhook—charge.complete→paid/payment_failed, everything else →ignored. Omise webhooks are unsigned, so it fails closed on a supplied signature/secret and the README says loudly to confirm byGET /charges/{id}.capability.json—chargeModes: [push],currencies: {thb: {min: 2000}}. Drift-tested againstCapabilities().gateway.json/webhooks.json/certify.json) — the language-neutral behavior contract; a future TS twin must replay the same files.Verified against the live Omise test API (2026-07-09)
Not just mocked — three targeted live probes, each captured in the code/README:
live_test.go) — real PromptPay charge →pending+ scannable QR atsource.scannable_code.image.download_uri(the coded path).invalid_charge400 gives the real minimum: 2000 satang (฿20), now declared honestly instead of invented.2019-05-29) does not dedup onIdempotency-Key; a repeated key minted a second charge. Because PromptPay is push this is not a double-collect (a retry mints a second unpaid QR that expires; reconcile by the paid webhook's charge id) — documented inCharge's doc + README rather than hidden. The key is still sent verbatim so dedup engages on versions that honor it.Testing
make checkgreen. 37 subtests inadapters/omise:robustness_test.go: transport failure / connection drop → error, verbatim idempotency key on retry, malformed/empty webhook bodies → error (the cases the declarative harness can't express).Known follow-ups (tracked in the adapter README)
GET /charges/{id}confirm-by-lookup helper for the unsigned-webhook path.@monetakit/omiseTS twin.Scope deliberately excludes Omise's card pull rail (would add
chargeMode "pull"+Vault) — a separate adapter/PR.