Skip to content

chore: version packages - #12

Closed
github-actions[bot] wants to merge 42 commits into
mainfrom
changeset-release/main
Closed

github-actions[bot] wants to merge 42 commits into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@payenv/connector-cinetpay@0.3.0

Minor Changes

  • d5f1486: CinetPay payouts.

    • Payouts by CinetPay transfer (POST /v1/transfer) from the balance of the recipient's
      country account: MTN, Moov, Orange Money, Wave, Free, T-Money and Airtel depending on
      the country (payoutMethods option, DEFAULT_PAYOUT_METHODS).
    • Payout status lookups by merchant reference, so a transfer that timed out is checked
      before any fallback. A too-low CinetPay balance maps to INSUFFICIENT_BALANCE, safe to
      fall back.
  • a5d32f6: CinetPay connector.

    • @payenv/connector-cinetpay (new): collections with CinetPay's API v1 in Benin, Burkina
      Faso, Côte d'Ivoire, Mali, Niger, Senegal, Togo, Cameroon and Guinea, by direct push
      (mobile_money) or CinetPay's hosted page (hosted_page, redirect next action). Status
      lookups by merchant reference, so a push that timed out is checked before any fallback.
    • @payenv/core: refresh keeps a payment's next action (e.g. a redirect URL) while the
      provider still reports requires_action without a new one.

Patch Changes

  • Updated dependencies [a5d32f6]
  • Updated dependencies [4daec16]
    • @payenv/core@0.3.0

@payenv/connector-fedapay@0.3.0

Minor Changes

  • 4daec16: Payouts.

    • @payenv/core: payenv.payout({ amount, recipient, idempotencyKey }) sends money with the
      same guarantees as collections (idempotency, fallback only when it is proven that no money
      was sent). New error code INSUFFICIENT_BALANCE.
    • @payenv/connector-fedapay: payouts in Benin (MTN, Moov, Celtiis) and payout status
      lookups. "Opération non autorisée" with HTTP 403 now maps to ROUTE_UNSUPPORTED.

Patch Changes

  • Updated dependencies [a5d32f6]
  • Updated dependencies [4daec16]
    • @payenv/core@0.3.0

@payenv/core@0.3.0

Minor Changes

  • a5d32f6: CinetPay connector.

    • @payenv/connector-cinetpay (new): collections with CinetPay's API v1 in Benin, Burkina
      Faso, Côte d'Ivoire, Mali, Niger, Senegal, Togo, Cameroon and Guinea, by direct push
      (mobile_money) or CinetPay's hosted page (hosted_page, redirect next action). Status
      lookups by merchant reference, so a push that timed out is checked before any fallback.
    • @payenv/core: refresh keeps a payment's next action (e.g. a redirect URL) while the
      provider still reports requires_action without a new one.
  • 4daec16: Payouts.

    • @payenv/core: payenv.payout({ amount, recipient, idempotencyKey }) sends money with the
      same guarantees as collections (idempotency, fallback only when it is proven that no money
      was sent). New error code INSUFFICIENT_BALANCE.
    • @payenv/connector-fedapay: payouts in Benin (MTN, Moov, Celtiis) and payout status
      lookups. "Opération non autorisée" with HTTP 403 now maps to ROUTE_UNSUPPORTED.

@payenv/connector-kkiapay@0.3.0

Patch Changes

  • Updated dependencies [a5d32f6]
  • Updated dependencies [4daec16]
    • @payenv/core@0.3.0

jules-mahounou and others added 30 commits September 26, 2026 21:28
Vision, architecture draft, connectors list, roadmap, Apache 2.0
license, contributing guide (DCO), code of conduct, security policy,
governance, architecture decision records 0001-0004, and GitHub
issue/PR templates.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
docs: set project contact address for security and conduct reports
npm rejects the unscoped `payenv` name (too similar to an existing
package), so all packages will be published under the `@payenv` scope.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
docs(roadmap): mark phase 0 setup steps as done
- TypeScript monorepo (pnpm workspaces, TypeScript 7, Vitest, Biome)
- Money model in integer minor units with ISO 4217 exponents
- Payment methods, unified statuses and transitions
- Error taxonomy with retry classes (safe_to_fallback, do_not_retry, ambiguous)
- Connector and capability interfaces
- Capability-aware router with the priority strategy
- Attempt engine enforcing ADR 0004: never try another connector while a
  previous attempt could still succeed; ambiguous attempts are resolved by
  checking the status with the same provider
- Idempotency (atomic store create), refresh, lifecycle events
- In-memory store for development and tests
- CI on Node 20/22/24 with a DCO sign-off check

The randomized invariant test tracks independently whether each provider
proved that no money moved, and was checked against injected double-charge
bugs.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
- Collections in Benin (XOF): MTN (mtn_open), Moov (moov), Celtiis (sbin),
  using the verified 3-step flow: create transaction, token, push
- Errors before the push are safe to fall back (no customer prompt exists);
  errors during the push are ambiguous and resolved by a status lookup
- Status mapping, HTTP error mapping, configurable operators per country
- Webhook signature verification (HMAC-SHA256, anti-replay, constant-time
  comparison) using Web Crypto, so it runs on any runtime
- Opt-in sandbox test driven by a local .env file
- QUIRKS.md separating verified behaviors from points to verify

core: add AttemptContext.reportProviderRef so an attempt that times out
after the provider created the transaction can still be resolved.

docs: roadmap entry for the PHP / Laravel ecosystem.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
feat(core): bootstrap @payenv/core with the safe-fallback attempt engine
feat(connector-fedapay): add FedaPay mobile money connector
The first real sandbox run confirmed authentication, transaction creation
and token generation, and that a non-activated operator fails at the push
with "Opération non autorisée", correctly mapped to ROUTE_UNSUPPORTED.

- FEDAPAY_SANDBOX_NETWORK selects the operator (mtn, moov, celtiis)
- Clear failure message when the operator is not activated
- QUIRKS.md: record what the sandbox verified

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
The FedaPay sandbox removed its per-operator test servers (MTN, Moov...):
every push must go to the single `momo_test` mode. Pushing to `/mtn_open`
in the sandbox fails with "Opération non autorisée", which an earlier
commit wrongly attributed to operator activation.

- Sandbox maps every network to `momo_test` by default, so application
  code is identical in sandbox and live
- Sandbox tests use the documented test numbers and wait for the final
  status: +22964000001 settles as succeeded, any other number as failed
  (declined → CUSTOMER_DECLINED). Both pass against the real API.
- QUIRKS.md corrected and updated with what the sandbox verified

Source: https://docs.fedapay.com/fr/integration-api/sending-requests#serveur-de-test

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
`pnpm demo` starts a small web page (Node built-in http, no framework) that
collects a sandbox mobile money payment through Payenv and follows its
status live. It masks phone numbers, refuses live keys, and shows the
webhook verification endpoint. Verified against the FedaPay sandbox:
+22964000001 settles as succeeded, another number as failed.

test(connector-fedapay): always use the documented sandbox success number,
so a leftover FEDAPAY_SANDBOX_PHONE in .env can no longer break the test.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
A payment accepted as pending and later declined showed
`status: failed` with `outcome: accepted` on its attempt (found while
testing the FedaPay demo). `refresh` now updates the attempt outcome
(failed / accepted), and clears the payment error once the provider
confirms an unresolved payment.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
To check that a real USSD prompt reaches a phone. Real money, so:
- explicit opt-in (DEMO_LIVE=yes) and a separate FEDAPAY_LIVE_SECRET_KEY;
  a sandbox key is refused in live mode and a live key in sandbox mode
- amounts capped by DEMO_LIVE_MAX_AMOUNT (default 200, at most 1000 XOF)
- warning in the terminal and on the page
- the server now only listens on 127.0.0.1

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Found with the founder's first live payments (2026-10-05):
- a canceled MTN payment carried `last_error_code: INSUFFICIENT_FUND_ERROR`,
  but Payenv only reported a generic CUSTOMER_DECLINED;
- a Celtiis payment canceled on the phone stayed `pending` at FedaPay with
  `last_error_code: API_ERROR`, and Payenv showed no reason at all.

core: PayenvError gains `providerCode`, serialized with the error, so every
connector can keep the provider's own code.

connector-fedapay: map `last_error_code` (INSUFFICIENT_FUND_ERROR →
INSUFFICIENT_FUNDS, API_ERROR → PROVIDER_UNAVAILABLE, others kept as
providerCode), also on transactions that stay pending. Status is never
changed from what FedaPay reports. Tests reproduce both live transactions;
verified against them read-only.

docs: QUIRKS (verified live), README guidance for payments stuck in pending.
demo: shows the reason while pending and what to do after a minute.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
The full live transaction shows that `last_error_message` is the
operator's raw SOAP answer: the status query succeeded and the operator
still reported `TransactionStatus: "Initiated"`. API_ERROR therefore
means "no final status from the operator", not an outage: map it to
UNKNOWN_ERROR with an accurate message. The raw dump is kept in
`error.raw` (never serialized).

QUIRKS: FedaPay schedules an expiration for pending transactions
(`metadata.expire_schedule_jobid`, `expired_at`).

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
A live `transaction.canceled` webhook showed the shape
`{ name, object, object_id, account, entity }`, where `entity` is the
full transaction. `parseFedaPayWebhook` normalizes it with the same
mapping as status lookups, deriving the status from the transaction
rather than the event name, so unknown event names still work.

- INSUFFICIENT_FUND_ERROR message now reflects MTN's actual reason
  (low balance, or a limit reached, or not allowed)
- demo: a verified webhook triggers payenv.refresh (webhooks are hints);
  verified end to end with a signed and a forged webhook
- QUIRKS: webhook shape, and a warning that webhooks carry merchant and
  payer personal data

Test fixtures are scrubbed of account details, phone numbers and tokens.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
- Changesets (fixed versioning for core and connectors); packages bumped
  to 0.1.0 with generated changelogs
- Release workflow publishing through npm trusted publishing (OIDC, no
  token, provenance), disabled until NPM_TRUSTED_PUBLISHING is "true"
- DCO check skips the release bot's version commits
- Each package ships LICENSE, NOTICE (Apache 2.0 section 4) and its
  sources (source maps resolve)
- docs/GETTING_STARTED.md: install, first payment, webhooks, statuses,
  errors, fallback, production checklist
- README: install command and guide link; CONTRIBUTING: release process

Verified by installing the packed tarballs in a fresh project: ESM
import, CJS require, and strict TypeScript without skipLibCheck.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
chore(release): prepare v0.1.0
Some providers (Kkiapay, and most checkout-style aggregators) cannot start
a payment from the server: the customer starts it in the provider's
widget, and the server verifies it afterwards.

- NextAction `widget` ({ provider, params }) tells the front end which
  widget to open
- Capability `widget` + CollectRequest `supportedWidgets`: widget routes
  are only used when the front end can open them; they can serve as a
  fallback after a server-push provider failed safely
- ProviderResult `merchantReference` and `amount`, recorded on attempts
- payenv.confirm(key, providerRef) links the provider transaction after
  checking with the provider that it carries this payment's merchant
  reference and amount, so a customer cannot confirm an order with
  another (e.g. cheaper) transaction; rejected confirmations leave the
  payment unchanged
- refresh leaves a widget payment untouched until it is confirmed
- new error code REFERENCE_MISMATCH

Anti-fraud tests were checked against removing each verification.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Kkiapay payments start in its widget; the official server SDK can only
verify (`/api/v1/transactions/status`) and refund. The connector:
- returns a `widget` next action with the widget parameters (public key
  only; partnerId = the payment's idempotency key), only when the front
  end lists `kkiapay` in supportedWidgets
- verifies transactions server-side (status, amount, partnerId, failure
  codes) for payenv.confirm
- verifies (constant-time shared secret) and parses webhooks
- opt-in sandbox tests driven by .env

demo: loads the Kkiapay widget, "Simuler une panne FedaPay" makes FedaPay
fail before any money can move so Payenv falls back to the widget, and
the widget's transaction id is confirmed server-side. Verified with the
server: fallback, refusal without widget support, rejected confirmation
leaves the payment unchanged, normal path stays on FedaPay.

docs: ADR 0005, architecture, getting started, connectors, quirks, and a
changeset (minor) adding the connector to the fixed version group.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
The sandbox answers an unknown transaction id with HTTP 400 and
`{ "status": "TRANSACTION_NOT_FOUND" }`, not 404 as assumed. Only this
explicit answer now counts as "not found", whatever the HTTP status.
Found by the opt-in sandbox test, which now passes.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Payenv keeps requiring E.164 (a number without its country is ambiguous,
and could charge the wrong person), but customers type local numbers.
`toE164(phone, country)` converts them, with per-country rules: the
leading 0 is a trunk prefix in Nigeria, Ghana, Kenya…, but part of the
number in Benin (+22901…) and Côte d'Ivoire.

demo: accepts local numbers (via toE164), pre-fills the Kkiapay test
number when the FedaPay outage is simulated, and shows widget failures.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
An apostrophe in "n'a bougé" closed a JavaScript string inside the page
template, so the whole inline script failed to load and the form was
submitted natively (the page just reloaded). Use a typographic
apostrophe.

To catch this class of bug: the page moves to page.ts, and a test parses
its inline script in every demo configuration (it fails on the original
bug).

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
FedaPay (simulated outage) → Kkiapay widget → payenv.confirm succeeded.
The status response echoes the widget's partnerId and reports the amount
requested (fees apart), which is what confirm's anti-fraud checks rely
on. The opt-in sandbox tests pass against the real API.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
payenv.payout({ amount, recipient, idempotencyKey, ... }) sends money
from the merchant's provider balance to a recipient. It runs through the
same engine as collections: idempotency (atomic, concurrent-safe), and
fallback to another connector only when it is proven that no money was
sent. A double payout is money lost, so the double-charge invariant
applies unchanged.

- Operation 'payout', PayoutRequest, optional Connector.payout; only
  connectors implementing it are used for payouts
- status lookups and refresh use the payment's operation
- new code INSUFFICIENT_BALANCE (the merchant's balance at a provider):
  safe to fall back, another provider's balance may suffice
- an idempotency key cannot be reused across collect and payout
- confirm is reserved to widget collections

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Uses the flow verified on live payouts: POST /payouts (payout modes mtn,
moov, sbin — not the collection slugs), then PUT /payouts/start. Money
can only leave the balance at `start`:
- errors before it are safe to fall back; "Solde insuffisant" on start
  maps to INSUFFICIENT_BALANCE (safe: another provider's balance may do)
- a timeout or 5xx on start returns `unknown` with the payout id, so
  Payenv checks GET /payouts/:id and never sends it again elsewhere
- payout statuses mapped (sent → succeeded; pending/started/... →
  pending; failed/declined; canceled), never guessed as failed

"Opération non autorisée" with HTTP 403 (the real sandbox answer when
payouts are not enabled) now maps to ROUTE_UNSUPPORTED instead of
AUTHENTICATION_FAILED; both are safe to fall back.

Kkiapay has no API to send money to customers (its "payouts" are the
merchant's own scheduled withdrawals): documented, not implemented.

docs: READMEs, quirks, getting started, connectors, roadmap, changeset.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
A status lookup that still reports requires_action without a new next action
no longer erases the one the customer has to follow (e.g. a hosted page URL).

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
Collections with CinetPay's API v1 (api.cinetpay.net / api.cinetpay.co) in
nine countries, by direct push or CinetPay's hosted page. Status lookups use a
merchant reference derived from the attempt, so a push that timed out is
checked before any fallback. Built from the official SDK; not yet verified
in the sandbox (see QUIRKS.md).

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 247bc99 to ce734a5 Compare October 16, 2026 14:13
Payouts by CinetPay transfer (POST /v1/transfer, API v1) from the balance of
the recipient's country account, with networks from a separate payout methods
table (Orange Money and Wave included: the recipient has nothing to do).

Same guarantees as collections: the transfer is a single call, so an uncertain
answer (timeout, server error, unreadable status) is ambiguous and Payenv looks
the transfer up by its merchant reference (GET /v1/transfer/{id}) before any
fallback. INSUFFICIENT_BALANCE is the merchant's CinetPay balance here and maps
to INSUFFICIENT_BALANCE, safe to fall back. Built from the official SDK; not yet
verified in the sandbox (see QUIRKS.md).

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from ce734a5 to ee0abf0 Compare October 16, 2026 14:16
jules-mahounou and others added 3 commits October 16, 2026 17:50
Rewrite both READMEs around the packages, features and commands that exist
now (the example used a fallback() helper that does not exist and listed
strategies and refunds that are only planned). Mark the planned parts of the
architecture, refresh the roadmap, and document CinetPay in the guides.

Signed-off-by: Jules Mahounou <234069818+jules-mahounou@users.noreply.github.com>
docs: describe what is available today
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.

1 participant