Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
check:
name: Lint, typecheck, test, build (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build

dco:
name: DCO sign-off
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check that every commit is signed off
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
missing=0
for sha in $(git rev-list --no-merges "$BASE..$HEAD"); do
if ! git log -1 --format=%B "$sha" | grep -q '^Signed-off-by: '; then
echo "::error::Commit $sha is missing a Signed-off-by line (use git commit -s)"
missing=1
fi
done
exit $missing
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Before `1.0.0`, minor versions may contain breaking changes.
## [Unreleased]

### Added
- 2026-09-26 — `@payenv/core`: money model (integer minor units, ISO 4217 exponents),
payment methods, unified statuses, error taxonomy with retry classes, connector and
capability interfaces, capability-aware router with the `priority` strategy, the
attempt engine enforcing the safe-fallback invariant (ADR 0004), idempotency,
`refresh`, lifecycle events, and an in-memory store.
- 2026-09-26 — TypeScript monorepo (pnpm, Vitest, Biome) and CI with DCO check.
- 2026-09-26 — Project started. Vision, architecture draft, roadmap, Apache 2.0
license, contributing guide, code of conduct, security policy, governance, and
initial architecture decision records.
22 changes: 20 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,23 @@ license explicitly allows it.

## Development setup

*Coming with Phase 1 (see [ROADMAP.md](docs/ROADMAP.md)).* The planned stack is
TypeScript, with a monorepo and a zero-dependency core built on `fetch`.
Requirements: Node.js 20.19+ and [pnpm](https://pnpm.io) 9.

```sh
pnpm install
pnpm check # lint + typecheck + tests
pnpm test:watch # tests in watch mode
pnpm format # auto-format with Biome
pnpm build # compile every package to dist/
```

Layout:

```
packages/
core/ @payenv/core — model, router, attempt engine (zero runtime dependencies)
src/
test/
```

The stack is TypeScript 7 (`tsc`), Vitest for tests, and Biome for lint and format.
2 changes: 1 addition & 1 deletion README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

🇬🇧 [Read in English](README.md) — *la version anglaise fait référence.*

**Statut :** 🌱 Conception — aucune version utilisable pour l'instant. Démarré le **26/09/2026**.
**Statut :** 🌱 Début du développement — le moteur existe, pas encore de connecteur ni de version publiée. Démarré le **26/09/2026**.
**Licence :** [Apache 2.0](LICENSE) — gratuit pour un usage personnel et commercial, pour toujours.

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

🇫🇷 [Lire en français](README.fr.md)

**Status:** 🌱 Early design — no usable release yet. Started on **2026-09-26**.
**Status:** 🌱 Early development — the core engine exists, no connector or release yet. Started on **2026-09-26**.
**License:** [Apache 2.0](LICENSE) — free for personal and commercial use, forever.

---
Expand Down
37 changes: 37 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!**/dist", "!**/*.md"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended"
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
12 changes: 7 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ Idempotency:

Payenv needs a small amount of state (payments, attempts, idempotency keys,
processed webhook IDs, success-rate stats). The store is an interface with
adapters: `memory` (dev/tests), then `postgres`, `redis`, `sqlite`, …
adapters: `memory` (dev/tests, built into the core), then `postgres`, `redis`,
`sqlite`, … Its `create` operation must be atomic, so that two concurrent calls
with the same idempotency key can never both start a charge.
The integrator owns the store and its data.

## 7. Observability
Expand All @@ -189,14 +191,14 @@ The integrator owns the store and its data.
wire to its own logs, metrics, or tracing (OpenTelemetry-friendly).
- No built-in telemetry sent anywhere.

## 8. Packaging (initial proposal)
## 8. Packaging

```
@payenv/core unified model, router, attempt engine, interfaces
@payenv/core unified model, router, attempt engine, interfaces,
and an in-memory store for development and tests
@payenv/connector-fedapay one package per connector
@payenv/connector-kkiapay
@payenv/store-memory
@payenv/store-postgres
@payenv/store-postgres persistent stores, one package each
```

## Open questions
Expand Down
11 changes: 6 additions & 5 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@

## Phase 1 — Core + first connector (`v0.1.0`)

- [ ] TypeScript monorepo, CI, lint, tests, release automation
- [ ] `@payenv/core`: unified model, error taxonomy, statuses
- [ ] Router with the `priority` strategy + capability filtering
- [ ] Attempt engine with the safe-fallback invariant, fully tested
- [ ] `@payenv/store-memory`
- [x] TypeScript monorepo, CI, lint, tests
- [ ] Release automation
- [x] `@payenv/core`: unified model, error taxonomy, statuses
- [x] Router with the `priority` strategy + capability filtering
- [x] Attempt engine with the safe-fallback invariant, fully tested
- [x] In-memory store (built into `@payenv/core`)
- [ ] `@payenv/connector-fedapay` (collect mobile money, status, webhooks)
- [ ] Getting-started guide and one runnable example

Expand Down
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "payenv-monorepo",
"private": true,
"description": "Payenv — one payment environment, many aggregators.",
"license": "Apache-2.0",
"type": "module",
"packageManager": "pnpm@9.15.9",
"engines": {
"node": ">=20.19"
},
"scripts": {
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check .",
"format": "biome check --write .",
"check": "pnpm lint && pnpm typecheck && pnpm test"
},
"devDependencies": {
"@biomejs/biome": "^2.5.14",
"typescript": "^7.0.2",
"vitest": "^5.0.2"
}
}
33 changes: 33 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# @payenv/core

The core of [Payenv](https://github.com/payenv-dev/payenv): the unified payment model,
the router, and the safe-fallback attempt engine.

> 🚧 Pre-release (`0.x`). The API may change.

```ts
import { createPayenv } from '@payenv/core';

const payenv = createPayenv({
connectors: [/* connectors, e.g. @payenv/connector-fedapay */],
// store: a persistent store in production (the default is in-memory)
});

const payment = await payenv.collect({
amount: { value: 5000, currency: 'XOF' },
method: { type: 'mobile_money', network: 'mtn', country: 'BJ', phone: '+22990000000' },
idempotencyKey: 'order_1234',
});

// payment.status: 'pending' | 'requires_action' | 'succeeded' | 'failed' | 'unknown' | ...
// A payment in 'unknown' must be reconciled later with payenv.refresh('order_1234').
```

**Guarantee:** Payenv never starts an attempt on another connector while a previous
attempt for the same payment could still succeed. See
[ADR 0004](https://github.com/payenv-dev/payenv/blob/main/docs/adr/0004-no-fallback-on-ambiguous-state.md).

Requires Node.js 20.19+ (ESM; `require()` works on Node 20.19+ as well), Deno, Bun, or
any runtime with `fetch`, `AbortController`, and `crypto.randomUUID`.

License: Apache-2.0
42 changes: 42 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@payenv/core",
"version": "0.0.0",
"description": "Payenv core: unified payment model, router, and safe-fallback attempt engine.",
"license": "Apache-2.0",
"author": "Jules Mahounou (https://github.com/jules-mahounou)",
"homepage": "https://github.com/payenv-dev/payenv#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/payenv-dev/payenv.git",
"directory": "packages/core"
},
"bugs": "https://github.com/payenv-dev/payenv/issues",
"keywords": [
"payments",
"payment-orchestration",
"mobile-money",
"fallback"
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
],
"engines": {
"node": ">=20.19"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"publishConfig": {
"access": "public"
}
}
Loading
Loading