Executive summary

Purpose and scope

Purpose

This specification document outlines the requirements for our core banking platform designed to support the oprations of Green-Got as a Payment Institution (Établissement de Paiement) operating under the supervision of the Autorité de Contrôle Prudentiel et de Résolution (ACPR). The platform will enable the institution to:

Scope

In Scope:

Out of Scope:

Regulatory framework

We must comply with a multi layered system of regulations

European Union Level Regulations

Payment Services

Data Protection

Anti-Money Laundering

French National Regulations

Regulatory Bodies

Legal Codes

Industry Standards and Security Requirements

Payment Schemes and Infrastructure

Key stakeholders

Internal Stakeholders

Management

Operational Teams

Support Functions

External Stakeholders

Supervisory Authorities

Technical & payments Partners

Operations Partners

Infra & tech Providers

Service Providers

Customers

System overview

Procedures

Procedures list

Assets freezing [en] [fr]

Alerting [en] [fr]

Tracfin Declaration [en] [fr]

Transaction Monitoring [en] [fr]

PSEE Selection (Prestataires de Services Essentiels Externalisés) [en] [fr]

Incident Management [en] [fr]

KYB (Know Your Business) [en] [fr]

KYC (Know Your Customer) [en] [fr]

PUPA (Plan d’Urgence et de Poursuite d’Activité) [en] [fr]

ISSP (Information Systems Security Policy) [en] [fr]

Risk management [en] [fr]

Ring fencing [en] [fr]

Fraud fight [en] [fr]

Authentication

Flow & architecture

Authentication — flow & architecture

How a customer proves who they are. This crate (src/authentication) owns three things:

The device-key path is a “build-our-own-FIDO” design — hardware-backed asymmetric keys signing server-issued challenges — without platform passkeys / WebAuthn. The mobile client contract is in mobile_integration.md.

Two device keys + a client-side PIN

A mobile install holds two ECDSA P-256 keypairs. The app PIN never leaves the device.

Request-signing keyDevice authentication key
Unlockdevice-unlock only, no per-use biometricFace ID OR the app PIN (local)
Signsevery authenticated request (RFC 9421)login / enrollment challenges
Public key storedon the device row (the device crate)as a DeviceKey credential (this crate)
Server holds a secret?no — only the public keyno — only the public key

(Shortened to signing key and auth key in the diagrams below.)

The app PIN is client-side only: it unlocks the device authentication key on the device and is never sent to or stored by the backend. The server only sees an authentication-key signature and is agnostic to how the user unlocked it.

Why two keys — a biometric-gated key prompts on every signature: fine for a once-per-session login, unusable for signing every request. Why asymmetric — the server stores only public keys, so no secret in the database can mint a valid request: database read access cannot impersonate a user.

Domain model

User (user crate) ──< LoginIdentifier (email / phone, Unverified|Verified)
     │ user_id
     ├──< Credential ── Password { hash } | DeviceKey { auth-key public key }
     └──< Session (one Valid per user_id + device_id)

Device (device crate) ── signing_public_key_spki  (request-signing key)

AuthFlow ──< PendingChallenge (factor + single-use nonce)

Key types:

The flow model — factors, gates, trust matrix

All non-password authentication is a server-driven challenge/response flow: the client is a responder and never encodes policy.

Endpoint surface (the flow lives in this crate; onboarding’s entry is in retail_api):

EndpointServicePurpose
POST /customer_auth/startauthenticationbegin a login flow
POST /customer_auth/respondauthenticationadvance any flow (login or onboarding)
POST /retail/onboarding/startretail_apicreate the user, begin enrollment
POST /retail/device/create_deviceretail_apiregister a device, bind the request-signing key
POST /customer_auth/login / POST /customer_auth/logoutauthenticationlegacy password login (below)

Every flow step returns the same FlowStepResponse: challenge_required (with the options to satisfy), complete (with a session token), or a uniform failed.

Onboarding (account creation + first login)

Orchestrated by retail_api (it owns user creation); this crate owns the credential / flow / session pieces.

device                       Retail API                 Customer Auth API
0. generate signing key + auth key
1. POST /retail/device/create_device ─▶ bind the signing key
  │◀─ { deviceId }
2. POST /retail/onboarding/start ─────▶ create User, start_device_enrollment
  │    (x-device-id, email, phone)
  │◀─ challenge_required (device_key_registration)
3. sign challenge with the auth key
4. POST /customer_auth/respond ──────────────────────────────────▶ verify, store
  │    (public_key_spki + signature)        DeviceKey, make Session
  │◀─ { status: "complete", token }

On complete, respond_to_challenge (enrollment branch) stores the email/phone login_identifiers (Unverified), creates the DeviceKey credential from the staged public key, assigns the device to the user, and creates the Session.

The device must already exist (step 1, POST /retail/device/create_device); start_onboarding only binds the flow to it. An unknown x-device-id — e.g. a stale client-cached id after a DB reset — returns 400 UnknownDevice so the client re-registers, rather than letting the auth_flow foreign key fail as a 500.

Daily login

device                                   Customer Auth API
1. POST /customer_auth/start (identifier) ─────▶ resolve user; only if this device
  │◀─ challenge_required (device_key)        has a DeviceKey bound for that user
2. unlock the auth key (Face ID or PIN), sign
3. POST /customer_auth/respond (signature) ────▶ verify vs the stored auth key
  │◀─ { status: "complete", token }

start_auth_flow returns the same failed for an unknown identifier and for a known identifier on a device with no DeviceKey, and runs the device + credential lookups unconditionally, so the two are timing-indistinguishable (no account/device enumeration). New/replacement-device enrollment on an existing account is out of scope in V0 — a device with no DeviceKey is rejected.

Password login (legacy /customer_auth/login)

POST /customer_auth/login is unchanged and still serves web / business / back-office. It runs the authenticate() use case, which is generic over an AuthStrategy (below):

  1. Resolve IdentifierKeyuser_id.
  2. strategy.verify(user_id, credentials) (Argon2id for PasswordStrategy).
  3. Resolve the device; reassign it to this user if it belonged to another (and revoke that user’s sessions on it).
  4. Supersede any existing Valid session on the same device.
  5. Create the session (lifetime by device type — see below).
  6. Write an AuthenticationAttempt (success or failure).

On an unknown identifier it returns the same error as a wrong password (no enumeration) and runs a dummy verify to equalize timing.

Sessions

            authenticate() / flow complete
                     Valid  ◀── validate_session() (slides the window)
   revoke()/logout     │ expires_at < now
                    Revoked

Per-request signing (RFC 9421 + RFC 9530)

Two layered middlewares protect authenticated routes:

  1. session_auth_middleware validates the session token and injects the Session + DeviceId.
  2. request_signature_middlewareiff the device has a request-signing key — verifies an ecdsa-p256-sha256 HTTP Message Signature over the components @method @authority @path @query authorization (plus content-digest when the request has a body), with created within ±5 minutes and a per-request nonce checked against auth_request_nonce for replay.

Devices without a request-signing key pass through unchanged — a flag-day-free rollout (today’s web/business cookie sessions are unaffected; each device flips to required the moment it registers a request-signing key). Because authorization is in the signed set, the signature sender-constrains the bearer: a stolen token is inert without the request-signing key.

@path/@query are reconstructed from the original (pre-nest) URI, so the client signs the full request path including the /{service} mount (e.g. /retail/contact/get_contacts), not the nest-stripped path. A rejected signature returns 403 (AuthError::Signature* — re-sign and retry; the session is still valid), distinct from the 401 session_auth_middleware returns when the session is gone (re-authenticate). Both use the standard { "error": "<Code>" } envelope.

The signed challenge payload

The exact bytes device authentication key signs, reconstructed in respond_to_challenge:

SIGNING_DOMAIN_TAG ‖ audience ‖ 0x00 ‖ factor_byte ‖ challengeId ‖ nonce ‖ deviceId ‖ sha256(identifier)

Audit

Every attempt — success and failure — is written to auth_authentication_attempt with AuthRequestContext (IP/geo, JSONB) plus its Gate and ScaCategory, shaped as a superset for a later 1:1 ClickHouse copy (no backfill). The gate / SCA category are derived from the credential kind at write time.

AuthStrategy (extension point)

Password verification goes through a pluggable trait so the core framework doesn’t know about specific credential types:

#[async_trait]
pub trait AuthStrategy: Send + Sync {
    type Credentials: Send;
    async fn verify(&self, user_id: &UserId, credentials: Self::Credentials)
        -> Result<(), AuthStrategyError>;
    fn credential_kind(&self) -> CredentialKind;
}

PasswordStrategy (Argon2id) is the V0 implementation. The device-key path does not use this trait — challenge verification needs flow context (nonce, device, identifier), so it is implemented as focused functions in device_key_strategy.rs that respond_to_challenge calls directly.

Design decisions

Code layout (DDD)

src/authentication/src/
  domain/                       -- entities + pure policy, no I/O
    auth_flow.rs                -- AuthFlow, FlowId, FlowStatus, FLOW_TTL
    pending_challenge.rs        -- PendingChallenge, ChallengeFactor, DEVICE_KEY_TTL
    gate.rs                     -- Gate + gate_of(factor) / Gate::of_credential
    trust_matrix.rs             -- next_step(), TRUST_MATRIX_VERSION (V0 policy)
    credential.rs               -- Credential, CredentialKind, CredentialEnvelope
    login_identifier.rs         -- LoginIdentifier, IdentifierKey, IdentifierType
    session.rs                  -- Session, session_duration(device_type)
    auth_attempt.rs             -- AuthenticationAttempt, AuthAttemptOutcome, ScaCategory
    strategy.rs                 -- AuthStrategy trait (password)
  application/use_cases/
    start_auth_flow.rs          -- login entry
    start_device_enrollment.rs  -- authorized first device-key enrollment
    respond_to_challenge.rs     -- the shared step loop (login + onboarding)
    flow_common.rs              -- FlowStepResult, issue_step, nonce
    create_session.rs           -- shared session-create (per-device revoke + TTL)
    authenticate.rs             -- password /customer_auth/login
    validate_session.rs, revoke_session.rs
  infrastructure/
    strategies/device_key_strategy.rs  -- signed_payload, verify, signing_audience
    strategies/password_strategy.rs    -- Argon2id, dummy_verify
    stores/                            -- auth_flow, pending_challenge, request_nonce,
                                          credential, login_identifier, session, attempt
    adapters/session_token.rs          -- HMAC-signed session tokens
  presentation/
    routes/auth_flow.rs         -- POST /customer_auth/start, /customer_auth/respond
    routes/login.rs, logout.rs  -- POST /customer_auth/login, /customer_auth/logout
    routes/extract.rs           -- shared device-info extraction
    flow_response.rs            -- FlowStepResponse wire shape
    middleware.rs               -- session_auth + request_signature middlewares
    service.rs                  -- CustomerAuthService, ServiceState

(The application/webauthn/ module is dead code — it references types that no longer exist — and is slated for removal; ignore it.)

Boundary with retail_api: onboarding’s HTTP entry (/retail/onboarding/start) and device registration (/retail/device/create_device) live in retail_api. This crate never creates users — it attaches credentials and sessions to a User that retail_api created via the user crate.

Out of scope (V0)

New/replacement-device enrollment, cross-device approval, soft-lock, recovery, NFC (Gate 3) / identity (Gate 4) / email (Gate 5) factors, three-tier (step-up) authorization, document-KYC gating, and app-level rate limiting — all deferred. The machinery (flows, gates, the exhaustive factor match, gate-tagged audit) is shaped so each lands as an additive change.

See also

Mobile integration

Mobile device-key authentication — integration guide

This guide is the contract for the mobile (Expo) team. The backend is the source of truth: it issues challenges, the client signs them, the server verifies. The client never encodes the trust matrix — it just fulfils whatever the server asks until the flow returns a session token.

Everything here is verified end-to-end by the backend tests in src/authentication (the per-request signing tests in presentation/request_signing_tests.rs and the flow tests in application/use_cases/respond_to_challenge.rs are executable references for the exact byte layouts below).

Scope. This is the V0 mobile path: create an account (onboarding) and do daily login with a hardware-backed device key, plus signing every authenticated request. New-device / replacement-device enrollment, NFC, cross-device approval and recovery are out of scope for V0.


1. The model: two keys + an app PIN

Each install holds two ECDSA P-256 key pairs. Both are non-extractable and hardware-backed (iOS Secure Enclave / Android Keystore + StrongBox). The app PIN is client-side only and never reaches the backend.

Request-signing keyDevice authentication key
Unlockdevice-unlock only, no per-use biometricFace ID OR the app PIN (local)
Signsevery authenticated HTTP request (RFC 9421)server-issued login / onboarding challenges
Registeredonce, at device creation (POST /retail/device/create_device)during the secure_device onboarding step
Server storespublic key on the device rowpublic key as a DeviceKey credential
Server holds any secret?No — only the public keyNo — only the public key

Why two keys. A Face-ID-gated key prompts biometrics on every signature — fine for a once-per-session login, unusable for signing every request. So the request-signing key (no per-use biometric) signs requests; the device authentication key (biometric/PIN-gated) signs the login challenge.

Why asymmetric. The server stores only public keys and holds no secret that can mint a valid request. An attacker with full database read access still cannot impersonate a user — they would need the request-signing key’s private half, which never leaves the Secure Enclave.

The app PIN is the mandatory baseline unlock for the device authentication key, set during onboarding. Face ID is an optional convenience layered on top, so a user who never enables biometrics still logs in via the PIN. The backend stores no PIN material, runs no PIN check, and needs no PIN lockout.


2. Key generation

Both keys are ECDSA P-256 (secp256r1), SHA-256. This is the only curve the iOS Secure Enclave can hold in hardware, the FIDO2/WebAuthn default, and Web Crypto’s standard ECDSA curve.

iOS (Secure Enclave). SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave, kSecAttrKeyTypeECSECPrimeRandom, 256 bits.

Android (Keystore). KeyGenParameterSpec with KeyProperties.KEY_ALGORITHM_EC, setDigests(DIGEST_SHA256), setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")), and setIsStrongBoxBacked(true) where available.

Non-extractability is mandatory for both keys. The private half must never be exported; you only ever export the public key (SPKI DER) and produce signatures on-device.


3. PIN handling (client-side only)

The 6-digit PIN never leaves the device. It is one of the local ways to unlock the device authentication key. To stop a stolen keystore file from being brute-forced offline, wrap the device authentication key with KDF(PIN + R) where R is a non-extractable Secure-Enclave / StrongBox secret — each guess then requires a hardware operation and cannot be parallelised off-device.

You may implement Face ID and the PIN as the same device authentication key (unlocked either way) or as two separate device authentication keys. If two, register both public keys — the backend accepts more than one DeviceKey credential per user.


4. Encodings (read this before anything else)

ThingEncoding
Public key (request-signing key and device authentication key)SPKI DER, then base64
Challenge signature (device_key, secure_device)ECDSA-SHA256 ASN.1 DER, then base64 (either S parity accepted)
Per-request signature (RFC 9421)ECDSA-SHA256 raw r‖s (64 bytes), then base64
nonce, identifier_hash in challenge paramsbase64 (decode to raw bytes before use)

Two different signature encodings: challenges use DER, per-request signing uses raw r‖s (the RFC 9421 / JOSE form). Don’t mix them up.

S parity: the server accepts either parity, so you don’t need to normalize — send whatever your platform emits.


5. Device registration (request-signing key) — first launch

Before any auth, register the device and bind the request-signing key. Until a device has a request-signing key, its requests are not signature-checked; the moment the request-signing key is registered, signing becomes mandatory for that device.

POST /retail/device/create_device
Content-Type: application/json

{
  "appVersion": "1.0.0",
  "platform": "ios",
  "osName": "iOS",
  "osVersion": "18.2",
  "deviceManufacturer": "Apple",
  "deviceModel": "iPhone 16",
  "signingPublicKeySpkiB64": "<base64(SPKI-DER of the request-signing key public key)>"
}
200 OK
{ "deviceId": "dvc_…" }

Persist the device ID. Send it as the x-device-id header on every later request. From now on, sign every authenticated request with the request-signing key (see §7) — including the auth-flow requests themselves once a session exists.


6. The flow loop

Daily login is a server-driven flow: call POST /customer_auth/start, then repeatedly POST /customer_auth/respond fulfilling one of the offered options each step, until the response is complete. First-launch onboarding is create-first: POST /retail/onboarding/start creates the onboarding and returns a prospect token; device-authentication-key registration happens later in the secure_device onboarding step.

Endpoints

PurposeRequest
Create an onboardingPOST /retail/onboarding/start — body { "offerId": "...", "country": "FR", "legalForm": "..." }, header x-device-id
Daily loginPOST /customer_auth/start — body { "identifier": { "kind": "email", "value": "..." } }, headers x-client-type: mobile, x-device-id
Advance login flowPOST /customer_auth/respond — body below
Advance onboardingPOST /retail/onboarding/submit_step — body { "onboardingId": "...", "step": { "stepCode": "...", ... } }

These are full paths, and the leading segment is the service mount. Onboarding and device registration live under the Retail API (/retail/…); auth start and respond live under the Customer Auth API (/customer_auth/…). Use the host the backend gives you for each.

FlowStepResponse (what the Customer Auth flow returns)

Discriminated by status:

// More to do — fulfil any ONE option
{
  "status": "challenge_required",
  "flowId": "flow_…",
  "options": [
    {
      "challengeId": "chl_…",
      "factor": "device_key",
      "params": {
        "nonceB64": "",                // present for signing factors
        "deviceId": "dvc_…",
        "identifierHashB64": ""
      }
    }
  ],
  "expiresAt": "2026-06-04T17:42:00Z"
}

// Done — store the token
{ "status": "complete", "flowId": "flow_…", "token": "" }

// Refused — uniform, reveals nothing about why
{ "status": "failed", "flowId": "flow_…", "error": "unauthenticated" }

In V0 each step has exactly one option, but code against options[] as a list: pick the first factor you can satisfy, sign it, post it.

ChallengeResponse (the body of POST /customer_auth/respond)

{
  "flowId": "flow_…",
  "challengeId": "chl_…",
  "response": { "factor": "device_key", "signatureB64": "" }
}

Mapping factor → UI


7. The signed challenge payload (device authentication key)

For the Customer Auth device_key login challenge, sign these exact bytes (byte-for-byte, or verification fails):

"GG-AUTH-v1\n"
  || audience
  || 0x00
  || factor_byte
  || challengeId
  || nonce
  || deviceId
  || identifier_hash
FieldValue
"GG-AUTH-v1\n"the literal 11 bytes 47 47 2D 41 55 54 48 2D 76 31 0A (note the trailing \n)
audiencethe environment string green-got/<environment>/authgreen-got/production/auth, green-got/staging/auth, or green-got/development/auth — matching the environment your build targets. <environment> is the logical environment, not the host: staging, sandbox and every PR-preview stack all use staging (so a staging build’s audience works unchanged against any preview at *.staging.green-got.co). It is not the API host, and carries no version (the version is in the GG-AUTH-v1 tag)
0x00one separator byte
factor_byte0x02 for device_key
challengeIdUTF-8 bytes of challengeId (e.g. chl_…) verbatim
nonceraw bytes of base64-decode(params.nonceB64)
deviceIdUTF-8 bytes of params.deviceId (e.g. dvc_…) verbatim
identifier_hashraw bytes of base64-decode(params.identifierHashB64) (this is SHA-256(identifier); you do not compute it — use what the server sent)

Then: ECDSA-P256/SHA-256 sign → DER encode → base64 → put in response.signatureB64.

Pseudocode:

payload  = b"GG-AUTH-v1\n" + b"green-got/production/auth" + b"\x00"   // audience for the env you target
         + factor_byte
         + challengeId.utf8
         + base64_decode(nonceB64)
         + deviceId.utf8
         + base64_decode(identifierHashB64)
sig_der  = ecdsa_p256_sha256_sign(keyB_private, payload)   // DER (either S parity)
signatureB64 = base64(sig_der)

For the Retail secure_device onboarding step, sign the issued prefill.challenge string bytes directly with the device authentication key. Submit:

{
  "stepCode": "SecureDevice",
  "publicKeySpki": "<base64(SPKI-DER public key)>",
  "signature": "<base64(DER signature over the challenge string)>"
}

8. Per-request signing (request-signing key) — RFC 9421 + RFC 9530

Once a flow returns a token, every authenticated request must carry an Authorization bearer plus an RFC 9421 HTTP Message Signature made with the request-signing key. A request with a valid bearer but no/invalid signature is rejected with 403 (re-sign and retry — the session is still valid; see §9). Algorithm: ecdsa-p256-sha256.

Covered components (in this exact order)

@method  @authority  @path  @query  authorization

…plus content-digest appended only when the request has a body (see below). Plus the signature params created (unix seconds, within ±300s of server time) and a fresh per-request nonce.

The signature base

Build a newline-joined string. Each line is "<component>": <value>:

ComponentValue
@methodupper-case method, e.g. GET
@authoritythe Host header value
@paththe request path exactly as sent, including the service prefix — e.g. /retail/contact/get_contacts (not a bare /contact/get_contacts)
@query? + the raw query string (just ? when there is no query)
authorizationthe full Authorization header value, e.g. Bearer eyJ…
content-digestthe Content-Digest header value (only if a body is present)

The final line is always:

"@signature-params": (<covered list>);created=<ts>;nonce="<nonce>"

where <covered list> is the space-separated, double-quoted component names.

Body integrity (RFC 9530)

When there is a request body, add a Content-Digest header and include content-digest in the covered set:

Content-Digest: sha-256=:<base64(SHA-256(body))>:

Signature → headers

Sign the base bytes with the request-signing key, take the raw 64-byte r‖s form, base64 it:

Signature-Input: sig=("@method" "@authority" "@path" "@query" "authorization");created=1717520520;nonce="a1b2c3…"
Signature:       sig=:<base64(raw r‖s)>:

Use the same created/nonce/covered list in both the Signature-Input header and the @signature-params line of the base, or verification fails.

Worked example — signed GET /protected (no body)

GET /protected HTTP/1.1
Host: api.green-got.com
Authorization: Bearer eyJ…
Signature-Input: sig=("@method" "@authority" "@path" "@query" "authorization");created=1717520520;nonce="9f2c…"
Signature: sig=:MEUCIQ…(base64 of 64 raw bytes)…:

signature base that was signed:

"@method": GET
"@authority": api.green-got.com
"@path": /protected
"@query": ?
"authorization": Bearer eyJ…
"@signature-params": ("@method" "@authority" "@path" "@query" "authorization");created=1717520520;nonce="9f2c…"

Worked example — signed POST with a body

Add Content-Digest and the extra covered component:

POST /contacts HTTP/1.1
Host: api.green-got.com
Authorization: Bearer eyJ…
Content-Type: application/json
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig=("@method" "@authority" "@path" "@query" "authorization" "content-digest");created=1717520520;nonce="7b13…"
Signature: sig=:MEQCIB…:

with base:

"@method": POST
"@authority": api.green-got.com
"@path": /contacts
"@query": ?
"authorization": Bearer eyJ…
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("@method" "@authority" "@path" "@query" "authorization" "content-digest");created=1717520520;nonce="7b13…"

9. Token storage, retries, errors


10. Platform fallbacks


11. End-to-end walkthroughs

A. First-launch onboarding

  1. Generate the request-signing key. POST /retail/device/create_device with its public key → store the returned device ID.
  2. POST /retail/onboarding/start ({ offerId, country, legalForm }, header x-device-id) → response contains prospectToken and the created onboarding.
  3. Submit onboarding steps through POST /retail/onboarding/submit_step. When the next step is secure_device, first submit { "stepCode": "SecureDevice" } to receive the challenge in the step prefill.
  4. Generate the device authentication key, sign the challenge with it, then submit { "stepCode": "SecureDevice", "publicKeySpki": "...", "signature": "..." }.
  5. Set up the local Face ID / PIN unlock for the device authentication key on these screens.
  6. When contact verification completes the bootstrap set, the submit-step response includes a token. Store it. Done — the user is onboarded and logged in.

B. Daily login (Face ID or PIN)

  1. POST /customer_auth/start ({ identifier: { kind: "email", value } }, headers x-client-type: mobile, x-device-id) → challenge_required with one device_key option.
  2. Unlock the device authentication key — Face ID or PIN (user’s choice). Build the §7 payload (factor_byte = 0x02), sign it (DER).
  3. POST /customer_auth/respond with signatureB64complete with a fresh token.

C. A signed authenticated request

  1. Build the §8 signature base for the request (add Content-Digest if there’s a body).
  2. Sign with the request-signing key, raw r‖s, base64.
  3. Send the request with Authorization, Signature-Input, Signature (and Content-Digest when there’s a body). Valid → 200; bad/missing signature → 403 (re-sign & retry); expired/invalid session → 401 (start a new auth).

Quick reference

Web & password clients

Client integration guide (web / password login)

This covers the email/password path (POST /customer_auth/login) used by web and the business / back-office apps. The mobile retail app uses the device-key flow (account creation, Face ID / PIN login, per-request signing) — see mobile_integration.md. The end-to-end design is in authentication_flow.md.

Required headers

Every /customer_auth/login request must include:

HeaderRequiredDescription
x-client-typeYesweb or mobile
User-AgentYes (web)Sent automatically by browsers
x-device-fingerprintNo (web)Optional browser fingerprint, stored as metadata
x-device-idNo (mobile)Server-issued device ID from POST /retail/device/create_device, stored locally (e.g. iOS Keychain)

Generating x-device-fingerprint on web: use FingerprintJS:

import FingerprintJS from '@fingerprintjs/fingerprintjs'

const fp = await FingerprintJS.load()
const { visitorId } = await fp.get()
// send as x-device-fingerprint

Device registration

Web (automatic)

No action required. The backend creates a device and sets the device_id cookie automatically on the first request, before the handler runs. Subsequent requests include the cookie transparently.

Mobile — POST /retail/device/create_device

Before the first login a mobile client registers a device to obtain a server-issued device_id. This route also binds the request-signing key, so it requires signingPublicKeySpkiB64:

{
  "appVersion": "1.2.3",
  "platform": "ios",
  "osName": "iOS 18",
  "osVersion": "18.0",
  "deviceManufacturer": "Apple",
  "deviceModel": "iPhone 16",
  "signingPublicKeySpkiB64": "<base64(SPKI-DER of the device's request-signing key public key)>"
}

Response: { "deviceId": "dvc_..." }. Store it securely and send it as x-device-id on subsequent requests. See mobile_integration.md for key generation and the full device-key flow.


Login — POST /customer_auth/login

{ "method": "email_password", "email": "user@example.com", "password": "secret" }

Web response

Body is {"token": null} — the token is not exposed to JavaScript; it is set as an HttpOnly cookie:

Set-Cookie: token=<token>; HttpOnly; Secure; SameSite=None; Path=/
Set-Cookie: device_id=<device_id>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=31536000

The browser handles the cookies automatically. The device_id cookie was set on the first request and persists for 1 year; login refreshes it and associates the device with the user.

Mobile response (password clients, e.g. business app)

{ "token": "<token>" }

Store the token securely and attach it to every authenticated request.

Error responses

StatusBodyReason
400"InvalidCredentials"Wrong email or password (also returned for unknown email — no enumeration)
400"MissingClientType"x-client-type header absent
400"InvalidClientType"x-client-type value is not web or mobile
400"MissingUserAgent"User-Agent header absent (web only)
422Validation failed (invalid email format, password too long, etc.)

Authenticated requests

Web

The browser sends the HttpOnly cookie automatically with every same-origin request. No additional setup needed.

Mobile / token clients

Attach the token as a Bearer:

Authorization: Bearer <token>

If the device registered a request-signing key (mobile retail app), the request must also carry an RFC 9421 signature — a bearer alone is rejected with 403 (re-sign and retry; the session is still valid). A 401 instead means the session is gone — start a new auth flow. See mobile_integration.md § Per-request signing. Password-only clients on devices without a request-signing key are unaffected.

Using auth in other services

Using the auth middleware in other services

Tokens issued by customer_auth are valid across all services since they share the same signing key from PARAMETERS.session_signing_key.

1. Add the dependency

# Cargo.toml
authentication.workspace = true
env.workspace = true
utils.workspace = true

2. Update ServiceState

Add token_service and implement AuthMiddlewareState:

use authentication::{AuthMiddlewareState, infrastructure::adapters::session_token::SessionTokenService};

#[derive(Clone)]
pub struct ServiceState {
    pub db_pool: &'static db::Pool,
    pub token_service: &'static SessionTokenService,
}

impl AuthMiddlewareState for ServiceState {
    fn db_pool(&self) -> &db::Pool { self.db_pool }
    fn token_service(&self) -> &SessionTokenService { self.token_service }
}

3. Initialise in register()

use env::PARAMETERS;
use utils::Leak;

fn register(&self, RegisterOptions { db_pool, .. }: RegisterOptions) -> Self::Result {
    let token_service =
        SessionTokenService::new(PARAMETERS.session_signing_key.expose().as_bytes()).leak();

    let state = ServiceState { db_pool, token_service };
    // ...
}

4. Protect routes

use authentication::session_auth_middleware;
use axum::middleware;

let protected = Router::new()
    .route("/some/route", post(handler))
    .route_layer(middleware::from_fn_with_state(
        state.clone(),
        session_auth_middleware::<ServiceState>,
    ));

5. Access the session in handlers

The middleware injects a Session into request extensions. Extract it as an axum Extension:

use authentication::{Session, UserId};
use axum::Extension;

async fn handler(
    State(state): State<ServiceState>,
    Extension(session): Extension<Session>,
) -> ... {
    let user_id: &UserId = &session.user_id;
    // ...
}

6. (Mobile) require per-request signatures (request-signing key)

For services exposed to the mobile app, layer request_signature_middleware after session_auth_middleware (so the Session + DeviceId are already in the request extensions). It enforces the RFC 9421 HTTP Message Signature iff the device has a request-signing key — see authentication_flow.md § Per-request signing. ServiceState must already implement AuthMiddlewareState (step 2).

Both middlewares return the standard { "error": "<Code>" } envelope (AuthError). The status tells the client what to do: 401 = session gone → re-authenticate (session_auth_middleware); 403 = signature rejected → re-sign and retry (request_signature_middleware). Callers don’t handle these — the middleware short-circuits before the handler runs.

use authentication::{request_signature_middleware, session_auth_middleware};
use axum::middleware;

// The LAST .layer() is the outermost (runs first): session auth runs first and
// injects Session + DeviceId, then the signature middleware verifies against the
// device's request-signing key.
let protected = protected
    .layer(middleware::from_fn_with_state(
        state.clone(),
        request_signature_middleware::<ServiceState>,
    ))
    .layer(middleware::from_fn_with_state(
        state.clone(),
        session_auth_middleware::<ServiceState>,
    ));

retail_api’s retail_router.rs is the reference wiring.

Local testing

Local testing guide

Prerequisites


Creating a test user

A CreateTestUser seed is included in the standard seed set. Run it with:

gg db seed
# or, to reset and reseed from scratch:
gg db reset

This creates one user with fixed credentials:

FieldValue
Emailtest@example.com
Passwordpassword123

Business Domain

Bills

0. Documentation Index

Bills — Documentation Index

The bills crate owns accounts payable (AP): the supplier invoices a Green-Got business customer receives — the “receive invoices from other businesses” pillar of the e-invoicing feature. Inbound transport and the legal lifecycle live in the Plateforme Agréée crate, which emits the InboundInvoiceReceived event this crate consumes.

Design rule: a received e-invoice is a bills/ Bill (AP) — it is never an invoicing.invoice (AR).

Documents

Settled vs. open

1. Bills Overview

Bills Overview

This document defines the scope of the bills (accounts-payable) domain — the “receive invoices from other businesses” pillar of Green-Got’s French e-invoicing feature — and its relationship to the plateforme_agreee transport layer.

1. Terminology

Terms shared across the e-invoicing documentation set (PA, SC, PPF, DGFiP, Annuaire, Flux 1, Flux 10, Factur-X, UBL, CII, EN 16931, SIREN, SIRET, AFNOR XP Z12-012, CDAR) are defined once in the Plateforme Agréée glossary — see Plateforme Agréée — 1. Reform Overview. This document does not restate them. The terms specific to the bills domain are:

2. AP Scope — The Receiving Pillar

The bills crate owns the full lifecycle of invoices that a Green-Got business customer receives from their suppliers. The reception obligation under the French reform applies to all in-scope French assujettis (Annuaire-registered entities) from 2026-09-01, ahead of the phased issuance obligation — so receiving is the first pillar every in-scope customer needs. See Plateforme Agréée — 1. Reform Overview.

In scope:

Out of scope:

3. The Critical Boundary — A Received E-Invoice Is a Bill, Never an Invoice

Design rule: A received supplier e-invoice is an accounts-payable document. It becomes a Bill in the bills crate. It is NEVER an invoicing.invoice (which is exclusively accounts-receivable — documents the customer issued). There is no foreign key, no shared table, and no shared Rust type between received supplier invoices and issued invoices.

AR and AP are separate domains. The only substrate they share is the plateforme_agreee transport layer (which carries both directions) and the organisation / core_banking foundation. See Plateforme Agréée — 10. Integration Contracts §4 and b2brouter/5. Receiving Invoices.

Dimensioninvoicing.invoice (AR)bills.Bill (AP)
DirectionOutbound — issued by the customerInbound — received by the customer
Cash flowMoney in (collection)Money out (payment)
CounterpartyThe customer’s clientThe customer’s supplier
Settling transactionIncoming (credit) bank transactionOutgoing (debit) bank transaction
Owning crateinvoicingbills
NumberingGap-free, owned by invoicingNone — the supplier owns the number
Legal status authored byThe buyer (received via PA)Green-Got, the buyer (emitted via PA mark_as)

4. Relationship to Plateforme Agréée and Other Inbound Channels

plateforme_agreee is a transport and compliance layer; it does not author AP records. For inbound, it discovers a supplier e-invoice by scheduled polling / list-get reconciliation of B2Brouter (the authoritative inbound source of record), verifies and persists an inbound transmission, downloads the original document plus B2Brouter’s extracted structured data, and emits exactly one InboundInvoiceReceived event. It stops at the transmission; bills picks up from there and owns the Bill. See Plateforme Agréée — 10. Integration Contracts §4.

Design rule — polling is the authoritative inbound channel; the webhook is an early-poll hint only. Inbound reception is polling-authoritative: plateforme_agreee learns of a received supplier e-invoice by polling B2Brouter on a fixed cadence (the canonical cadence is hourly; see PA — 10. Integration Contracts §4) and reconciling the transmission list, not by trusting a webhook. The B2Brouter webhook, where it fires, is an optimisation that may accelerate an early poll — it is never the source of truth and never creates a Bill on its own. A lost or spoofed webhook therefore never loses or fabricates a supplier invoice: the next poll (and a nightly reconciliation backstop) is authoritative. PA dedupes inbound transmissions on the B2Brouter invoice id, so a webhook-accelerated poll and the scheduled poll converge on a single transmission. Where this overview and PA §4 differ, PA §4 wins. The detailed inbound polling schedule/SLA and the bills-side reception contract are in 2. Received Invoice Domain §2.

The PA is the regulated inbound channel, but it is not the only one. The bills crate also ingests bills through channels that never touch plateforme_agreee:

ChannelSourceStructured on arrival?
Plateforme AgrééeInboundInvoiceReceived from PA (Peppol / B2Brouter network / email-to-PA)Yes — B2Brouter delivers extracted structured data; the Bill skips OCR.
Email-forwardA document forwarded to a per-organisation reception address held by billsNo — unstructured PDF; bills OCRs it.
Manual uploadA member uploads a PDF in the appNo — unstructured PDF; bills OCRs it.
API importProgrammatic import (later phase)Depends on payload.

Design rule: Only the Plateforme Agréée channel involves plateforme_agreee and the regulated legal lifecycle. Email-forward and manual upload are bills-only channels: they produce a Bill, but there is no PA transmission and no AFNOR legal status to report unless and until the bill is reconciled against a PA-delivered counterpart (see §4.2). The structured-vs-OCR distinction is the main behavioural difference at creation time; all channels converge on the same Bill lifecycle once any cross-channel duplicate has been deduplicated (§4.1) and any PA counterpart reconciled (§4.2). See 2. Received Invoice Domain.

4.1 Deduplication and idempotency at creation (all channels)

Because the same supplier invoice can arrive more than once — re-forwarded email, re-uploaded PDF, a webhook-accelerated poll racing the scheduled poll, or the PA channel re-delivering — every channel must create a Bill idempotently. Without this, the same invoice yields duplicate Bills that can each be approved and paid → double payment.

4.2 Cross-channel reconciliation against a PA-delivered counterpart

From 2026-09-01 a customer can receive the same invoice both by email/upload (OCR Bill, no transmission) and later via the Plateforme Agréée (the regulated copy). The two must converge on one Bill carrying the legal trail.

4.3 Email-forward reception address contract

The email-forward channel ingests a document forwarded to a per-organisation reception address owned by bills:

5. Document-Set Map

DocScope
1. Bills Overview(this doc) AP scope, the AR/AP boundary, inbound channels, relationship to the transport layer.
2. Received Invoice DomainThe Bill aggregate, its fields, the AP lifecycle state machine, the AFNOR / B2Brouter status mapping for inbound, and the approval workflow.
3. Payment and MatchingPaying a bill on Green-Got rails, recording the AP-internal Paid state (the supplier-facing mark_as paid → AFNOR Encaissée is gated [open — provider-support]), and auto-attaching the outgoing transaction to the bill.

6. Related Documents

7. Sources

This is an internal-design overview; its regulatory claims (the reception obligation applying to all in-scope French assujettis (Annuaire-registered entities) from 2026-09-01, the AR/AP boundary, the inbound formats) are carried with primary citations in the Plateforme Agréée docs:

2. Received Invoice Domain

Received Invoice Domain

This document defines the Bill aggregate — how it is created from an inbound supplier e-invoice, the fields it carries, its accounts-payable decision lifecycle (the AFNOR-aligned received decision/arrival status set), the mapping of that lifecycle to the B2Brouter and AFNOR inbound statuses, and the approval workflow that gates acceptance and payment.

Design rule — supplier acceptance state is separate from Green-Got payment state (two distinct fields). A received bill carries two independent states on two distinct fields that must never be conflated (the F-06 hazard): the supplier-invoice acceptance/decision state (Bill.decision_status, surfaced to the supplier via PA) and the Green-Got bill-payment state (BillPayment.state, the internal money-movement lifecycle). The only PA-emittable buyer-side decisions for French received invoices are Accepted (Approuvée 205) and Refused (Refusée 210). A Green-Got payment moves BillPayment.state only; it does not advance decision_status and does not by itself emit any AFNOR status. “Fully paid” is BillPayment.state == PaymentSettled, not a decision_status value. The supplier-facing Encaissée (212) / mark_as paid path for received French invoices is not confirmed and is held [open — provider-support] — the four-state separation, the payment rails, and this open question are owned by 3. Payment and Matching §1.1.

1. Terminology

Shared reform terms (PA, SC, PPF, DGFiP, Flux 1, Factur-X, UBL, CII, EN 16931, AFNOR XP Z12-012, CDAR, SIREN, SIRET) are defined in the Plateforme Agréée glossary. Terms specific to this document:

2. Creating a Bill from InboundInvoiceReceived

When a supplier e-invoice arrives through the PA, plateforme_agreee discovers it via its scheduled polling / list-get reconciliation of the transmission (the authoritative inbound source of record — a webhook may accelerate an early poll but is never authoritative; see §4.1), persists an inbound transmission, downloads the original document, and emits InboundInvoiceReceived on its EVENT_BUS. The bills crate subscribes via a rules/ handler and creates the Bill. The event is a projection of polling-authoritative reception, not of a webhook signal. The event payload (authoritative contract: Plateforme Agréée — 10. Integration Contracts §6):

FieldMeaningUsed by bills to…
transmission_idThe PA inbound transmission idStore as the Bill’s back-reference to the legal trail.
organisation_idThe receiving customer’s organisationScope the Bill to the org; resolve approval routing.
document_refReference to the original supplier artefact (Factur-X / UBL / CII)Retain the original in the core S3 bucket bills/ subpart; document_ref points to it (see §2.1).
extracted_fieldsStructured supplier-invoice data (Factur-X/UBL/CII parsed)Populate the Bill fields — supplier identity, amounts, VAT, due date, lines.
received_atWhen the document was receivedStamp the Bill’s reception time.

Design rule: PA-delivered invoices arrive structured, so the Bill created from InboundInvoiceReceived skips OCR. (Email-forward and manual-upload channels — which never touch plateforme_agreee — still OCR; see 1. Bills Overview §4.)

Design rule: The Bill references the transmission (transmission_id) and the supplier master; it is owned by bills. It is never foreign-keyed to invoicing.invoice. The event carries ids and references, not the document bytes.

Design rule — creation is idempotent on every channel. Bill creation from InboundInvoiceReceived is idempotent on the B2Brouter invoice id the transmission carries (PA dedupes transmissions on the same id); the OCR channels dedupe on a per-organisation natural key plus a PDF content-hash guard, and an OCR Bill is reconciled onto its PA counterpart when both arrive. The full deduplication, idempotency, and cross-channel reconciliation contract is owned by 1. Bills Overview §4.1–§4.2; this document does not restate it.

2.2 Inbound polling schedule, SLA, and reconciliation

PA-channel reception is polling-authoritative (§4.1): plateforme_agreee owns the poller; bills consumes the resulting InboundInvoiceReceived event. The cadence/SLA below align to the canonical PA inbound contract (PA — 10. Integration Contracts §4); PA §4 wins where they differ.

2.3 Supplier master resolution

A Bill is grouped under a Supplier master (supplier_id, §3.1). On creation bills resolves the supplier from the extracted/OCR’d identity:

2.4 OCR mandatory-field contract

The OCR channels (email-forward, manual upload) populate the Bill from an OCR extraction that may be incomplete. The minimal contract for creating a Bill versus the fields mandatory for PA emission:

2.1 Storage of the original artefact

Design rule — the incoming original is retained in core S3. Inbound bills are accounts-payable documents Green-Got must keep (legal: piste d’audit fiable, retention 10 years accounting / 6 years VAT). The original supplier artefact (Factur-X / UBL / CII, or the OCR’d PDF for the email/upload channels) is stored in the core project S3 bucket, in the bills/ subpart (s3://<core-bucket>/bills/...); the Bill’s document_ref points to that object. bills resolves the original from core S3 for display and audit; the bytes are never carried on the event.

Design rule — keep only what we must. The retention policy is “when we don’t need to keep a document, we don’t keep it.” Incoming bills are the case where we have no choice — they are kept. (This contrasts with issued AR invoices, whose legal/archive download serves the stored transmitted/PA-generated legal artifact (legal_artifact_ref); on-the-fly regeneration from immutable DB data is a display/preview fallback, not the archived legal copy — see PA — 8. Archiving and Audit.)

3. The Bill Entity

A Bill is the structured representation of one received supplier invoice. Its fields are populated from extracted_fields (PA channel) or from OCR (email/upload channels), then normalised onto the same shape.

3.1 Fields

Field groupFieldNotes
Identityidbil_-prefixed time_sortable_id — the canonical Bill id prefix (analogous to inv_ / quo_ / cli_).
organisation_idOwning customer org.
sourceChannel: PlateformeAgreee | EmailForward | Upload | ApiImport.
transmission_id?PA inbound transmission (present only for the PlateformeAgreee source).
Supplier identitysupplier_idFK to the Supplier master.
supplier_nameLegal name (denormalised from the document).
supplier_siren9-digit legal entity id. Mandatory mention under the reform for PA-delivered invoices.
supplier_siret?14-digit establishment id, when the document carries it.
supplier_vat_number?Intra-EU VAT number.
supplier_iban?Creditor IBAN for payment (from the document’s payment means).
AmountscurrencyISO 4217.
total_excl_vatNet total, integer cents (i64).
total_vatVAT total, integer cents.
total_incl_vatGross total, integer cents — the amount due.
VATvat_breakdown[]Per-rate lines: { category_code (UNCL5305: S/Z/E/AE/K/G/O), rate, taxable_base, vat_amount }.
Datesissue_dateSupplier’s invoice date.
due_datePayment due date; drives payment scheduling.
Documentsupplier_invoice_numberThe supplier’s own number (the supplier owns it; bills never renumbers).
document_type_codeUNCL1001: 380 invoice, 381 credit note (avoir), 386 prepayment, etc.
document_refReference to the original Factur-X / UBL / CII artefact, retained in the core S3 bucket bills/ subpart (§2.1).
Linesline_items[]{ description, quantity, unit_price_excl_vat, vat_category_code, vat_rate, line_total }.
Lifecycledecision_statusThe AP decision / arrival lifecycle status — the buyer-decision + arrival states only (see §4). Payment progress is NOT here; it lives on BillPayment.state (3 §1.2).
timestampscreated_at, received_at, updated_at.

Design rule: Monetary amounts are integer cents (i64); the gross total_incl_vat is the amount a payment must settle. The structured data is the parsed projection; the original document (document_ref) is the legally retained artefact, stored in the core S3 bucket bills/ subpart (§2.1). Its full PII-bearing bytes are retained for the applicable statutory minimums6 years (VAT) and 10 years (accounting/commercial) — under the piste d’audit fiable obligation; any retention beyond that minimum is the Green-Got product archive promise (a distinct purpose with its own lawful basis and controls), not an indefinite legal-obligation hold. Permanent retention is limited to non-PII hashes / audit evidence. See Plateforme Agréée — 8. Archiving and Audit.

Design rule: A credit note received from a supplier (document_type_code = 381, an avoir) is a Bill like any other, but it reduces what the customer owes rather than increasing it. It is never modelled by mutating the original bill.

4. The AP Lifecycle — AFNOR-aligned Received Decision Status Set

A Bill’s decision_status is the canonical internal received decision/arrival status: a 1:1 image of the AFNOR XP Z12-012 codes that express a buyer decision or document arrival on a received invoice. It carries only the decision + arrival axis. Payment progress is a separate axis owned by BillPayment.state (Unpaid → PaymentScheduled → PaymentInitiated → PaymentSettled, or PaymentFailed / PaymentReturned; see 3 §1.1, §1.2) — the two are never collapsed into one field. Green-Got adopts 100% of the AFNOR decision/arrival statuses (mandatory + recommended). The canonical set is defined in PA — 5. Lifecycle Statuses §9 (received internal set); this document mirrors it and PA/5 is authoritative wherever the two differ.

Design note — deference to PA/5 on 211/212. PA/5 §9/§10 (read-only, authoritative) lists PaymentTransmitted (211) and Paid/Settled (Encaissée 212) within its received internal set and maps AFNOR 211/212. On the bills side those are not Bill.decision_status values: they are the transmission/payment projection (the BillPayment.state money-movement axis), surfaced through the transmission, not buyer decisions held on the bill. PA/5’s §10 INBOUND row already maps Encaissée (212) to “Paid (bills, internal)” — i.e. the internal payment state, not a decision — so the two documents agree on substance. Where any residual wording differs, PA/5 wins and this is the deference point.

Received internal decision/arrival status set (decision_status):

Received (arrival, pre-decision), Invalid (Rejetée 213 on a received doc), Available (Mise à disposition 203), TakenInCharge (Prise en charge 204), Accepted (Approuvée 205), PartiallyApproved (Approuvée partiellement 206), Disputed (En litige 207), Suspended (Suspendue 208), Completed (Complétée 209), Refused (Refusée 210).

PaymentTransmitted (Paiement transmis 211) and Paid/Encaissée (212) are NOT decision_status values — see the design note above and §4.1.

Design rule — fully paid is a BillPayment.state, not a decision_status. A bill being fully paid is BillPayment.state == PaymentSettled, not a decision_status value. Only Accepted (205) and Refused (210) are confirmed PA-emittable buyer decisions for French received invoices. Whether a mark_as paid on a received transmission legally yields Encaissée (212) is not confirmed and is tagged [open — provider-support]; until staging proves it, payment tracks the Green-Got payment state only (BillPayment.state) and emits no AFNOR status — it does not advance decision_status. This question and the four-state separation are owned by 3. Payment and Matching §1.1.

Design rule — decision_status vs. orthogonal concerns. decision_status is the AFNOR-aligned decision/arrival code only. Payment progress and operational steps are not decision codes — they are orthogonal axes/fields:

stateDiagram-v2
    [*] --> Received: InboundInvoiceReceived (or email/upload ingest)
    Received --> Invalid: B2Brouter validation failed (received-doc invalid)
    Received --> Available: made available in-app (203)

    Available --> TakenInCharge: acknowledged, processing started (204)
    TakenInCharge --> Accepted: content approved → mark_as accepted (205)
    TakenInCharge --> PartiallyApproved: partial acceptance (206)
    TakenInCharge --> Refused: mark_as refused (210)
    TakenInCharge --> Disputed: contested with supplier (207)
    TakenInCharge --> Suspended: suspended pending document (208)

    Suspended --> Completed: document provided (209)
    Completed --> TakenInCharge: resume

    Disputed --> Accepted: dispute resolved, accept
    Disputed --> Refused: dispute resolved, refuse

    PartiallyApproved --> Accepted: resolved to full acceptance (corrected supplier doc)
    PartiallyApproved --> Refused: resolved against
    PartiallyApproved --> Disputed: escalated to dispute

    Accepted --> Disputed: dispute raised after acceptance

    Invalid --> [*]: terminal (returned/handled out of band)
    Refused --> [*]: terminal

    note right of Accepted
        decision_status is the buyer-decision + arrival axis ONLY.
        Accepted / Refused are the only confirmed PA-emittable
        buyer-side decisions Green-Got emits via mark_as,
        becoming the AFNOR legal status Approuvée (205) /
        Refusée (210). Approval gates Accepted (§6); it is
        not a status. PAYMENT IS A SEPARATE AXIS: paying an
        Accepted bill advances BillPayment.state
        (Unpaid → … → PaymentSettled), NOT decision_status.
        Fully paid = BillPayment.state == PaymentSettled.
        PaymentTransmitted (211) / Encaissée (212) are the
        BillPayment / transmission projection, not decision_status
        values; mark_as paid → 212 is [open — provider-support]
        (§4.1, [3 §1.1]).
    end note

The payment axis runs in parallel on the same Bill and is not part of this diagram — it is owned by BillPayment.state (Unpaid → PaymentScheduled → PaymentInitiated → PaymentSettled, or PaymentFailed / PaymentReturned), specified in 3. Payment and Matching §1.1–§1.2, §2.3. Paying only ever moves BillPayment.state; decision_status stays Accepted throughout (it is never made terminal by payment).

Key characteristics:

Design rule — full-payment-only AP (MVP); partial / multi-tranche payment is deferred. In the MVP a bill is settled by a single full payment: only an Accepted bill is payable, BillPayment.state == PaymentSettled is reached only when the bill is paid in full, and a bill is not split across multiple settling transfers. PartiallyApproved is recorded as a decision_status but is not a payable state — it must resolve to Accepted/Refused/Disputed first. Partial / multi-tranche supplier payment (accepted payable amount, outstanding balance, cumulative settlement) is a deferred, post-MVP capability and is not modelled here; this mirrors the AR side, where the customer-facing payment link is full-amount-only. Failed/returned transfers may be re-attempted (PaymentFailed/PaymentReturned → re-initiate), but a settled BillPayment.state == PaymentSettled is terminal (and decision_status remains Accepted, never made terminal by payment).

4.1 Mapping to B2Brouter Received Statuses and AFNOR Legal Statuses (Inbound)

On the inbound side Green-Got is the buyer, so it emits the acknowledgement and accept/refuse decisions (the mirror of outbound, where Green-Got receives the buyer’s decision). The only confirmed PA-emittable buyer targets are accepted and refused; the paid/Encaissée (212) target is [open — provider-support] (see below).

Design rule — polling is the authoritative inbound contract. Inbound status and arrival are learned by polling the transmission via plateforme_agreee (the authoritative inbound contract); webhooks, where present, are an optimisation, not the source of truth. The canonical INBOUND mapping is owned by Plateforme Agréée — 5. Lifecycle Statuses §6, §10 (IN rows). This table mirrors the PA/5 §10 IN rows from the Bill’s point of view; where this table and PA/5 differ, PA/5 wins:

The first ten rows are the Bill.decision_status projection (buyer-decision + arrival). The last two AFNOR codes — 211 and 212 — are not decision_status values: they are the BillPayment / transmission (payment) projection of the money-movement axis, listed here only to complete the AFNOR mapping. PA/5 §10 maps Encaissée (212) to the internal payment state (“Paid (bills, internal)”); on the bills side that is BillPayment.state == PaymentSettled, never a decision_status value.

Bill.decision_status (bills)B2Brouter received statusAFNOR legal statusHow it is driven
Receivedreceived (or new if manually imported)(arrival — pre-decision)Scheduled polling / list-get reconciliation of the transmission via plateforme_agreee (a webhook may accelerate an early poll but never creates the bill), or manual ingest.
InvalidinvalidRejetée (213)B2Brouter: validation issue on the received document.
AvailablereceivedMise à disposition (203)Recipient PA (us): made available in-app.
TakenInChargemark_asPrise en charge (204)Green-Got (buyer): acknowledged, processing started.
AcceptedacceptedApprouvée (205)Green-Got (buyer): mark_as accepted after approval.
PartiallyApprovedmark_asApprouvée partiellement (206)Green-Got (buyer): partial acceptance.
Disputedmark_asEn litige (207)Green-Got (buyer): dispute raised.
Suspendedmark_asSuspendue (208)Green-Got (buyer): suspended pending document.
Completedmark_asComplétée (209)Green-Got (buyer): document provided.
RefusedrefusedRefusée (210)Green-Got (buyer): mark_as refused after approval.
(annotation only)annotated(no change)Green-Got: internal note; no notification, no status change.

Payment-axis projection (NOT decision_statusBillPayment.state / transmission):

Source (BillPayment.state)B2Brouter received statusAFNOR legal statusHow it is driven
PaymentInitiatedmark_asPaiement transmis (211)Green-Got (payer): payment initiated on GG rails. Internal-only today (no confirmed native target).
PaymentSettledpaidEncaissée (212)Green-Got (payer): payment settled on GG rails. mark_as paid → Encaissée is [open — provider-support] for received French invoices; until confirmed, this tracks the Green-Got payment state only (BillPayment.state == PaymentSettled) and emits no AFNOR status. This is not a decision_status value.

Design rule — internal adoption is 100%; confirmed PA emission is accepted / refused only. The rows marked are the recommended buyer-side / payment statuses (204, 206, 207, 208, 209, 211) for which there is no documented native B2Brouter mark_as (its generic mark_as reference lists new | paid | accepted | refused | annotated, but the DGFiP receiving flow confirms only accepted and refused for the buyer). They are recorded internally only (and, where useful, as a non-legal annotation). Fail closed: never project an unsupported legal status onto a “closest” supported one — emitting Disputed/Suspended/PartiallyApproved (decision axis), or Paiement transmis (211) (the BillPayment.state == PaymentInitiated projection), to the supplier/PPF as accepted, refused, or paid would assert a false legal lifecycle fact. Such statuses stay internal/annotation-only unless B2Brouter staging/support confirms an exact native wire value and its legal meaning (see PA — 5. Lifecycle Statuses §10). The row marked (BillPayment.state == PaymentSettledpaid → Encaissée 212) is not confirmed for received French invoices and is held [open — provider-support] — until staging proves it, settlement is the Green-Got payment state only and no mark_as paid is emitted on the PA channel. Neither 211 nor 212 is a Bill.decision_status value. This is the intended design: a canonical internal model carrying the full AFNOR subtlety, with mappers projecting it onto each external representation — AFNOR (legal), B2Brouter (transport), and the business app (presentation) — see PA — 5. Lifecycle Statuses §1.1. Whether B2Brouter can transmit each † status (and the ‡ paid target) natively is a confirm-on-staging item (see PA — Uncertainties). PA/5 §10 is authoritative for this narrowing; the paid/Encaissée open question is owned by 3. Payment and Matching §1.1, §3.

Design rule: Accept / refuse (and the recommended buyer-side transitions) are surfaced to the supplier only through plateforme_agreee’s mark_as call on the transmission. bills decides; plateforme_agreee transmits. bills never calls B2Brouter directly. The AFNOR legal status is owned by the transmission, not by the Bill (the Bill holds the AP-internal status; the legal status is derivable via the transmission). The B2Brouter mark_as mechanics are specified in Plateforme Agréée — b2brouter/6. Statuses and Webhooks.

Design rule: Paying a supplier moves the Green-Got payment state; it is not a PA-emittable acceptance decision and does not by itself emit mark_as paid. Whether a paid received invoice can surface as AFNOR Encaissée (212) at all is [open — provider-support]. If confirmed on staging, that Encaissée would be driven by Green-Got’s own payment data (B2Brouter performs no settlement) and routed through plateforme_agreee. The payment rails, the four-state separation, and this open question are detailed in 3. Payment and Matching §1.1, §3.

Design rule — annotation strategy for the internal-only (†) statuses. Each internal status falls into exactly one projection bucket toward the PPF/supplier:

Fail closed: the choice of bucket is never “project onto the closest supported legal status.” A status whose native wire value and legal meaning are unconfirmed is annotation or internal-only, never re-mapped onto accepted/refused/paid. Which statuses are annotated vs internal-only is a product/operational decision per status; the default is internal-only, with annotation enabled only where the supplier benefits and staging confirms annotated behaviour.

Design rule — only PA-delivered bills have a reportable AFNOR status. A Bill from the email-forward or manual-upload channel has no PA transmission and therefore no reportable AFNOR legal status until it is reconciled onto a PA-delivered counterpart (1 §4.2). Its decision_status (and BillPayment.state) is meaningful internally (it drives approval/payment) but is not projected to the PPF. Only a Bill with a transmission_id participates in legal-status emission.

Design rule — Bill.decision_status → AFNOR legal-status derivation. The legal status is owned by the transmission, derived from Bill.decision_status by the mapper above and emitted through plateforme_agreee (never by bills directly). The mapper is the decision-axis table in this section: Accepted → Approuvée (205), Refused → Refusée (210), Invalid → Rejetée (213), with the decision statuses annotated/internal-only. Payment never derives a decision_status and never makes it terminal: because the received-side paid → 212 mechanism is [open — provider-support], a Bill whose BillPayment.state reaches PaymentSettled has no new legal status emitted — its last emitted legal status remains Accepted (205) (it was Accepted before it could be paid), and decision_status stays Accepted. Settlement advances the Green-Got payment state (BillPayment.state) only; it does not map to 212 today and is not a decision_status value.

5. Invariants

6. Approval Workflow

Approval gates acceptance of a bill, and an optional second gate (four-eyes release) gates the payment itself. The two gates are independent.

Design rule — content approval gates acceptance. A Bill cannot be moved to Accepted (Approuvée, 205) until an authorised member approves its content. Approval is a precondition of the accept decision, not a separate AFNOR status — it does not appear in the status set (§4); it is the gate that permits the TakenInCharge → Accepted transition.

Design rule — optional four-eyes release gates payment. Independently of content approval, a four-eyes release optionally gates the payment/transfer itself above a configurable threshold: a second authorisation, by a different member, required before the outgoing transfer is released. So the flow is: content-approval → Accept; then, for amounts above the release threshold, four-eyes release → pay. The release gate is a payment control, not a status; it is owned, with the payment-rail and fraud/scoring controls, by 3. Payment and Matching §2.

6.1 MVP defaults (concrete, implementable, revisable)

These are the MVP defaults for the approval gate. They are deliberately conservative and revisable by product/config; nothing here is a regulatory constraint. The full per-org policy editor (custom roles, per-category thresholds, N>2 approvers) is deferred — these defaults ship first.

6.2 Commands, events, and the approved/refused/paid state machine

The approval and decision actions are modelled as AP-internal commands with org-scoped audit events; per the AP-matching invariant, none of these is a cross-crate eventbus event.

Command (bills use_case)PreconditionEffectAudit event (AP-internal)
SubmitForApprovalTakenInChargeRoutes the bill per ApprovalPolicy; records required approver count.BillSubmittedForApproval
ApproveBillpending approval; approver ≠ makerRecords one approval; when the policy’s N is met → Accepted + mark_as accepted.BillApproved, then BillAccepted
RefuseBillpending approval or DisputedRefused + mark_as refused.BillRefused
DisputeBillTakenInCharge / AcceptedDisputed.BillDisputed
ReleasePaymentAccepted; releaser ≠ approvers (if over four-eyes threshold)Authorises the transfer; hands off to payment (3).BillPaymentReleased

The accept/refuse decision flow drives Bill.decision_status (§4); the payment leg advances BillPayment.state (a separate axis, never decision_status) and is owned by 3. Payment and Matching (commands/events for initiating, settling, attaching, and the paid/Encaissée [open — provider-support] question are defined there — not duplicated here).

Design rule — approval state is preserved, never silently voided, on Refused and Dispute. Recorded approvals are piste d’audit fiable evidence:

6.3 Delayed, scheduled, and batch behaviour (MVP defaults)

6.4 Support-intervention flow (MVP)

A Green-Got support actor may need to act on a stuck bill (e.g. a transmission the customer cannot resolve, a mis-ingested document). The MVP rule:

Support actionAllowed from-statesGuard
Re-trigger inbound polling / re-fetch the transmissionany non-terminal stateno state change; idempotent (§2 dedupe)
Re-run extractionReceived, Available, TakenInCharge, Disputed, Suspendedfield-correction audit (§2.4); never on a terminal bill
Move to DisputedTakenInCharge, Accepted, PartiallyApprovedpreserves approval state (§6.2); not from Refused/Invalid/Paid
Move to SuspendedTakenInChargeresolves via Completed (§4)
Re-link a mis-attached transactionbill not in a terminal state (Refused/Invalid/Paid)see terminal-state guard below

7. Related Documents

3. Payment and Matching

Payment and Matching

This document specifies how a received supplier invoice is paid on Green-Got’s own rails, the mandatory payment controls that gate every outgoing transfer, how (and whether) that payment drives any supplier-facing AFNOR status, and how the resulting outgoing bank transaction is automatically attached back to the Bill.

Design rule — paying a supplier never auto-emits a legal acceptance status. Settling a Bill on Green-Got’s rails is a money-movement fact. It is not the buyer’s legal accept/refuse decision, and it does not by itself emit any PA-emittable AFNOR status. The only PA-emittable buyer-side decisions are Accepted (Approuvée 205) and Refused (Refusée 210) (see §1.1 and 2. Received Invoice Domain §4.1). The supplier-facing Encaissée (212) / B2Brouter mark_as paid path for received French invoices is not confirmed by the DGFiP receiving flow and is held [open — provider support] (§3).

1. Terminology

Shared reform terms are in the Plateforme Agréée glossary. Terms specific to this document:

1.1 Four separate states — never conflate them

Paying a supplier touches four distinct states that must be modelled and stored separately. Collapsing them is the F-06 hazard: a Green-Got payment must not be allowed to imply a legally valid PA acceptance status.

#StateOwner / storeValuesWhat changes itLegally PA-emittable?
1Green-Got bill-payment statebills (BillPayment)UnpaidPaymentScheduledPaymentInitiatedPaymentSettled (or PaymentFailed / PaymentReturned)The outgoing transfer lifecycle on core_banking (§2.3).No — purely internal money movement.
2Supplier-invoice acceptance/decision statebills (Bill.decision_status) → plateforme_agreee transmissionThe AFNOR decision/arrival set — `ReceivedAvailableTakenInCharge
3B2Brouter local received stateB2Brouter (via plateforme_agreee)new, accepted, refused, paid†, annotatedmark_as calls routed through plateforme_agreee.Transport-layer; mirrors (2).
4PPF / AFNOR legal statusDGFiP PPF (the authoritative legal layer)the AFNOR XP Z12-012 code surfaced to the supplierDerived from the transmission’s mark_as.Authoritative legal layer.

Design rule — payment moves state (1) only. A completed transfer advances the Green-Got bill-payment state (1) and may auto-attach the transaction (§4). It does not by itself move (2), (3) or (4). In particular it does not emit mark_as paid.

Design rule — paid† / Encaissée for received French invoices is [open — provider support]. B2Brouter’s generic mark_as reference lists paid as a received-invoice state, but the DGFiP / B2Brouter France receiving guide only confirms accepted and refused as valid buyer targets. Until staging proves a French received invoice legally accepts paid → Encaissée (212), Green-Got does not emit mark_as paid for the PA channel; the bill-payment state (1) advances internally only. See §3.

1.2 The BillPayment entity

A BillPayment records one outgoing-transfer attempt that settles (or attempts to settle) a Bill. It owns the Green-Got bill-payment state (state 1 of §1.1) and the VoP audit trail (§2.1). A Bill may carry more than one BillPayment only as re-attempts after a PaymentFailed/PaymentReturned; at most one reaches PaymentSettled (full-payment-only, §4.1).

Field groupFieldNotes
Identityidbpay_-prefixed time_sortable_id.
bill_idFK to the Bill this payment settles.
organisation_idOwning customer org (denormalised for scoping).
Transfertransfer_idcore_banking credit-transfer id this BillPayment initiated.
railInstantSepa | ClassicSepa | ScheduledSepa (§2).
amount_centsSettled amount = the bill’s total_incl_vat (full-payment-only).
currencyISO 4217 (EUR in the MVP).
creditor_ibanSupplier IBAN paid (snapshot of supplier_iban at release).
execution_date?For scheduled SEPA: the chosen future execution date.
StatestateUnpaidPaymentScheduledPaymentInitiatedPaymentSettled, or PaymentFailed / PaymentReturned (§1.1, §2.3).
Four-eyesreleased_by?The Releaser who authorised the transfer (above the §6.1 threshold or on a VoP override).
VoP auditvop_resultmatch | close_match | no_match | other (§2.1).
vop_matched_name?Name returned by the beneficiary bank.
vop_evaluated_atWhen VoP was evaluated (before initiation).
vop_override_by?The Releaser who overrode a non-match outcome (≠ maker).
vop_override_reason?The captured override reason (§2.1; immutable once written).
Timestampstimestampscreated_at, initiated_at?, settled_at?, failed_at?, updated_at.

Design rule: The VoP audit fields, released_by, and the override fields are part of the piste d’audit fiable and are immutable once written (§2.1). amount_cents always equals the bill’s total_incl_vat in the MVP (no partial payment).

2. Payment Initiation — Green-Got’s Own Rails

Design rule: B2Brouter performs no payment processing or settlement. Its payment fields (payment_method, iban, remittance_information, …) are metadata only, and its paid status is set from payment information Green-Got owns. See Plateforme Agréée — 10. Integration Contracts §8 and Plateforme Agréée — b2brouter/6. Statuses and Webhooks.

Paying a Bill is an outgoing transfer from the customer’s account — initiated through core_banking to the supplier’s IBAN, for the bill’s gross amount (total_incl_vat). Three rails exist:

RailWhen it executesTypical use
Instant payment (SEPA Instant Credit Transfer)Within seconds, any timeThe default — pay the supplier immediately
Classic SEPA (SEPA Credit Transfer)Next business dayFallback when instant is disabled, or no urgency
Scheduled SEPAOn a chosen future date (e.g. due_date − N days)Pay on the due date without manual follow-up

Design rule — instant by default. Instant payment is the default rail — Green-Got wants every supplier paid instantly; it is the better outcome. The customer may instead pick classic or scheduled SEPA (e.g. to pay exactly on the due_date).

Design rule — the scoring engine may downgrade instant (AML / fraud). Instant payment is the default, but the choice is subject to Green-Got’s scoring / risk engine, which — for AML and fraud reasons — may:

The supported rails (instant / classic / scheduled SEPA) and the scoring rules are owned by core_banking and the risk engine; bills requests a payment (defaulting to instant) and honours the engine’s verdict — allowed, downgraded (instant disabled → offer classic/scheduled), or needs-document (collect the document, then release). The precise core_banking transfer-initiation API, batch-pay, and scheduling parameters are to be finalised against the core_banking transfer contract.

Invariant: Only an Accepted bill is payable (acceptance is itself approval-gated). A bill cannot be paid from Received, Invalid, Refused, or Disputed.

Invariant: A payment proceeds on a rail only if the scoring/risk engine permits it. An instant-payment request the engine forbids is offered as classic or scheduled SEPA instead, and any required confirming document must be supplied before the transfer is released.

Design rule — optional four-eyes release on the transfer. Content approval gates acceptance (see 2. Received Invoice Domain §6); independently, an optional four-eyes release gates the payment/transfer itself above a configurable threshold — a second authorisation, by a member distinct from the content approver, required before the outgoing transfer is released. The flow is therefore: content-approval → Accept, then (for amounts above the release threshold) four-eyes release → pay. The release gate is a payment control, not an AFNOR status. The release threshold is the MVP >€10,000 default authoritatively defined in 2. Received Invoice Domain §6.1 (EUR-only in the MVP); this document defers to §6.1 and does not redefine it.

Invariant: Above the four-eyes release threshold, an Accepted bill is payable only after a second, independent authorisation of the transfer. Below the threshold, acceptance alone suffices to initiate payment (subject to the scoring/risk gate above).

2.1 Mandatory pre-release controls — VoP and scoring

Before any outgoing transfer is initiated, bills runs a fixed set of pre-release controls. The first and mandatory one is Verification of Payee (VoP) — the EU Instant Payments Regulation name/IBAN check (supplier_name vs supplier_iban) run via core_banking against the beneficiary bank. No credit transfer (instant, classic, or scheduled) is released until VoP has been evaluated.

Design rule — VoP is a mandatory pre-release gate. Every payment request passes through VoP before the transfer is initiated. VoP returns one of four outcomes:

VoP outcomeMeaningDefault effect on release
matchBeneficiary name matches the account holder.Pass — release may proceed (subject to scoring + four-eyes).
close_matchNear match (e.g. minor spelling / legal-form difference).Blocked until an authorised member overrides with a captured reason; instant rail allowed only after override.
no_matchName does not match the account holder.Blocked; fails closed off the instant rail — requires review and override, and falls back to a non-instant rail (classic/scheduled SEPA).
otherVoP unavailable / unsupported / inconclusive (e.g. non-reachable bank, non-SEPA).Blocked pending review; treated as not a match for instant-rail purposes (fail-closed) — release requires an override decision.

Design rule — fail-closed / instant downgrade. Instant payment is the default rail (§2), but VoP is authoritative over it: a no_match (and a fail-closed other) blocks the instant rail and downgrades the bill to review + a non-instant fallback. Only match clears the instant rail without an override. A close_match may keep instant only after an authorised override. The instant rail is never released on an unresolved VoP result.

Design rule — override authority and reason capture. A match needs no override. A close_match or no_match may be released only via an explicit override by a Releaser (the four-eyes role, see 2. Received Invoice Domain §6.1) — never by the maker who initiated the payment. The override must capture a free-text reason; an override without a reason is rejected server-side. A no_match override additionally requires the bill to be above no other blocking control (scoring may still forbid it).

Design rule — VoP interacts with maker-checker (four-eyes). VoP and the four-eyes release gate are independent and cumulative, both evaluated before release:

Design rule — VoP audit fields. Every release records, on the BillPayment, the VoP audit trail: the VoP result (match | close_match | no_match | other), the matched name returned by the beneficiary bank, the VoP timestamp, and — when an override occurred — the overriding actor and the override reason. These fields are part of the piste d’audit fiable and are immutable once written.

Design rule — override-reason structure, retention, PII, immutability, visibility. The override reason is a structured field, not a free-floating string:

Invariant — no transfer without VoP. No outgoing credit transfer is initiated until VoP has been evaluated and recorded. A no_match (or fail-closed other) blocks the instant rail outright; release on any rail after a non-match outcome requires a Releaser override with a captured reason, and the maker may never override their own payment.

Invariant — VoP audit is mandatory. Every BillPayment that results in a released transfer carries the VoP result, matched name, VoP timestamp, and (where applicable) the overriding actor and reason; these are written before the transfer is initiated and are immutable thereafter.

2.3 Payment-state transition timing and atomicity

The bill-payment state (state 1 of §1.1) advances on observed core_banking transfer events, not optimistically. The triggers and their atomicity:

TransitionTriggerNotes
Unpaid → PaymentScheduledA scheduled SEPA payment is created with a future execution_date.The BillPayment is persisted with the chosen rail; no money has moved. Bill.decision_status stays Accepted until execution (and throughout).
Unpaid/PaymentScheduled → PaymentInitiatedcore_banking accepts the credit-transfer request (after the VoP + four-eyes gates pass).Advances BillPayment.state only; Bill.decision_status stays Accepted. (Projects to AFNOR Paiement transmis 211 on the transmission axis, §2 §4.1 — not a decision_status value.)
PaymentInitiated → PaymentSettledcore_banking confirms settlement of the outgoing transfer.Advances BillPayment.state to the terminal PaymentSettled and auto-attach runs (§4); Bill.decision_status stays Accepted.
→ PaymentFailed / PaymentReturnedcore_banking reports the transfer failed/returned.BillPayment.state records the failure; Bill.decision_status stays Accepted; a new BillPayment re-attempt may be initiated (§4.1).

3. Recording Full Payment — No PA-Facing Paid Signal Today (Encaissée gated)

Once the outgoing transfer is confirmed settled, BillPayment.state advances to PaymentSettled (full-payment-only, §4.1). This is an AP-internal payment state and is always recorded; it is a separate axis from Bill.decision_status, which stays Accepted. There is no PA-facing paid signal for received French invoices today (decision #14): a completed transfer moves the AP payment state only — it emits no mark_as paid, no cross-crate paid event, and no AFNOR status. Reflecting that payment to the supplier as the AFNOR legal status Encaissée (CDAR 212) is a separate, gated step that does not exist on the live path:

  1. bills records the bill’s BillPayment.state as PaymentSettled (the AP payment state only; decision_status is untouched). It emits no PA-facing paid signal — there is no unconditional payment signal to plateforme_agreee.
  2. If and only if a future staging run confirms that a received French invoice legally accepts mark_as paid → Encaissée (212), this becomes a distinct, gated event (never a generic unconditional signal): plateforme_agreee would then call B2Brouter POST /invoices/{id}/mark_as with { "state": "paid", "commit": "with_mail" } on the inbound transmission to surface Encaissée to the supplier — but this is [open — provider-support]. B2Brouter’s France DGFiP flow confirms only accepted / refused as valid mark_as targets for received French invoices; paid / CDAR 212 on the received side is documented only on the issued lifecycle and is not confirmed.

Design rule — the bill’s settled payment state never auto-asserts a PA legal status. Until B2Brouter support confirms that mark_as paid produces a valid CDAR 212 / Encaissée for French received invoices (tracked as [open — provider-support] in Uncertainties P-3), Green-Got records BillPayment.state == PaymentSettled internally only (and never advances decision_status) and does not emit a received-side mark_as paid. If/when confirmed, the Encaissée emission is driven by Green-Got’s own payment data and routed through plateforme_agreee (the transmission owner) — bills never calls B2Brouter directly. See the INBOUND mapping in Plateforme Agréée — 5. Lifecycle Statuses §6, §10 and the mark_as mechanics in Plateforme Agréée — b2brouter/6. Statuses and Webhooks.

Design rule: This Encaissée is the AP / inbound one (Green-Got, the buyer, has paid a supplier). It is not the AR / outbound Encaissée (a client paid the customer), whose payment data comes from AR collection (PaymentCollected) and is reported channel-specifically (the enriched 212/MEN for invoiced Flux-1 ops, Flux 10 only for non-invoiced). The two share an AFNOR label but belong to opposite directions and opposite crates. Inbound payment does not trigger the customer’s own Flux 10 obligation — e-reporting of payment data is the supplier’s concern, not the buyer’s. See Plateforme Agréée — 7. E-Reporting.

3.1 Acquisition e-reporting vs payment-data e-reporting

Two reporting concepts must be kept apart on the AP side. They are distinct and must never be collapsed into one event:

Design rule — AP owns acquisition reporting; AP never emits buyer-side payment data. The split is firm: bills is responsible for acquisition-data e-reporting on in-scope cross-border foreign-supplier bills (the ReportableAcquisitionRecorded event), and bills never emits a buyer-side payment-data event — paying a supplier stays AP-internal and triggers no payment-data signal and no PA-facing paid signal. The cross-border acquisition scope, cadence, and the acquisition-vs-payment-data taxonomy are owned by Plateforme Agréée — 7. E-Reporting; bills supplies the in-scope acquisition data and routes it through plateforme_agreee (the reporting owner) — it never reports to the DGFiP directly.

4. Auto-Attaching the Outgoing Transaction

The user’s requirement: pay a bill, “then the invoice should be automatically attached to the transaction.” When the outgoing credit transfer settles, core_banking produces an outgoing (debit) bank transaction. bills matches it back to the Bill it paid.

4.1 Matching criteria

CriterionHow it matches
AmountThe debit amount equals the bill’s total_incl_vat (same currency).
ReferenceThe transfer’s remittance reference (derived from supplier_invoice_number) appears on the transaction.
BeneficiaryThe transaction’s beneficiary IBAN equals the bill’s supplier_iban.

A transaction that bills itself initiated for a specific bill carries that bill’s id, so the match is normally deterministic (initiated payment ↔ resulting transaction). The amount / reference / beneficiary criteria are the reconciliation fallback for transactions ingested without that linkage.

Design rule — reference normalization. Before comparison, the remittance reference and supplier_invoice_number are normalised identically (trim, uppercase, strip separators/whitespace) so that formatting differences in how a bank echoes the reference do not defeat a match.

Design rule — time window. The reconciliation fallback only considers debit transactions within a bounded time window around the payment (initiation/execution date ± a few business days), to avoid matching an unrelated later transfer to the same supplier for the same amount.

Design rule — multi-match (collision) resolution. When the fallback criteria match more than one candidate Bill (e.g. two open bills to the same supplier for the same amount):

Design rule — no-match / orphaned transaction. A debit that matches no Bill (or only below the confidence bar) is left unattached and surfaced as an orphaned outgoing transaction for member review — it is never force-attached. Symmetrically, a Bill whose initiated transfer never produces a settling transaction stays in BillPayment.state == PaymentInitiated and is surfaced by the PaymentReminderWorkflow; its BillPayment.state is not auto-advanced to PaymentSettled (decision_status remains Accepted throughout).

Design rule — full-payment-only (MVP). A Bill is settled by a single full payment: its BillPayment.state reaches PaymentSettled only when paid in full, and is not split across multiple settling transfers. A bill may carry more than one BillPayment over time only as re-attempts (a PaymentFailed / PaymentReturned transfer followed by a re-initiated one), but at most one settling transfer brings it to the terminal PaymentSettled state. Partial / multi-tranche payment (accepted payable amount, outstanding balance, cumulative settlement across tranches) is a deferred, post-MVP capability and is not modelled here — mirroring the AR side, where the customer-facing payment link is full-amount-only. Each BillPayment links the Bill to one core_banking transfer attempt.

4.2 Contrast with AR matching

AP matching (bills, this doc)AR matching (invoicing)
Transaction directionOutgoing (debit — money out)Incoming (credit — money in)
What is settledA supplier Bill (we owe)An issued invoicing.invoice (we are owed)
Who initiates the transferGreen-Got (the payer)The customer’s client (the payer)
Legal-status effectNone today — no PA-facing paid signal for French received invoices; a confirmed mark_as paid → inbound Encaissée would be a distinct gated event [open — provider-support]Outbound Encaissée + Flux 10 payment-data e-reporting
Trigger eventnone cross-crate required (AP-internal) — ReportableAcquisitionRecorded is a separate cross-border acquisition event (§3.1), not a paid signalTransactionMatchedplateforme_agreee

See the AR counterpart, invoicing — 9. Transaction Matching, for the incoming/credit side. Because AR matching feeds the customer’s own Flux 10 obligation it emits TransactionMatched; AP matching does not, since the buyer has no payment-data e-reporting obligation here.

4.3 Pay → transaction → auto-attach

sequenceDiagram
    autonumber
    participant Member as Member (one-click pay)
    participant BILLS as bills (AP)
    participant CB as core_banking
    participant PA as plateforme_agreee
    participant B2B as B2Brouter (PA)
    participant Sup as Supplier

    Member->>BILLS: Pay Accepted bill
    Note over BILLS: Mandatory pre-release VoP gate (§2.1) — match / close_match / no_match / other;
no_match downgrades off instant; close_match/no_match require override (reason) + four-eyes;
blocks release until resolved BILLS->>CB: Initiate SEPA credit transfer (supplier IBAN, total_incl_vat, ref) CB-->>BILLS: Outgoing (debit) transaction created BILLS->>BILLS: Auto-attach transaction to Bill (amount / ref / beneficiary) BILLS->>BILLS: BillPayment.state → PaymentSettled (Green-Got bill-payment axis only) Note over BILLS: A completed transfer advances BillPayment.state ONLY (decision_status stays Accepted).
It does not by itself emit any AFNOR / PA legal status. rect rgb(245, 235, 220) Note over BILLS,Sup: [open — provider-support] — gated branch, NOT the normal path
(only if B2Brouter confirms mark_as paid → Encaissée for French received invoices) BILLS-->>PA: Signal paid (on the inbound transmission) PA-->>B2B: POST /invoices/{id}/mark_as { state: paid, commit: with_mail } B2B-->>Sup: AFNOR Encaissée (payment received) end Note over BILLS: Full-payment-only (MVP): one settling transfer → terminal PaymentSettled;
extra BillPayments only as re-attempts. Partial/multi-tranche deferred (post-MVP).

5. Invariants

6. Related Documents

Invoicing

0. Documentation Index

Invoicing — Documentation Index

The invoicing crate owns accounts receivable (AR): the quotes and invoices a Green-Got business customer issues. Transmission and the legal lifecycle live in the Plateforme Agréée crate; received supplier invoices (AP) live in bills. These documents are the source of truth for the AR domain.

Overview & model

Documents & numbering

Lifecycle, money & reconciliation

Other

To Be Created

1. Invoicing Overview

Invoicing Overview

This document defines the scope of the invoicing crate — Green-Got’s accounts-receivable (AR) domain that authors the quotes and invoices a customer issues to their own clients — and how it relates to the regulated transport layer in plateforme_agreee.

1. Terminology

The shared French-reform vocabulary (PA, SC, PPF, DGFiP, Annuaire, Flux 1, Flux 10, Factur-X, EN 16931, AFNOR XP Z12-012, …) is defined once in the Plateforme Agréée glossary; this doc set defers to it rather than redefining it. See Reform Overview — Terminology. This document adds only the AR-local terms:

2. AR Scope — What Invoicing Owns

The invoicing crate is the accounts-receivable domain. It owns the business documents a Green-Got customer issues to their clients and everything required to author, number, send, collect, and reconcile them.

In scope:

Design rule: invoicing is issuing only. It never receives, stores, or models a supplier invoice. The act of receiving is an accounts-payable concern owned by bills.

Invariant: An invoicing.invoice row is exclusively AR — a document the customer issued. No received supplier invoice is ever written to, or foreign-keyed from, the invoicing tables. See PA 10. Integration Contracts §4.

3. Relationship to Plateforme Agréée

invoicing and plateforme_agreee are separate crates with a clean split of responsibility: invoicing owns the business documents; plateforme_agreee owns the regulated transmission and the authoritative legal status. Green-Got’s legal posture is a Solution Compatible (SC) — a software layer — on top of B2Brouter, the certified Plateforme Agréée (PA). The SC authors and presents documents; the PA exchanges them and tracks their legal lifecycle.

Concerninvoicing (AR)plateforme_agreee (transport)
Authoring the classical invoice + generating the EN 16931 structured modelOwns
Gap-free numbering, finalization, immutabilityOwns
Credit notes (avoir)Owns
The regulated transmission recordOwns
Mapping EN 16931 → B2Brouter JSON; B2Brouter HTTP adapterOwns
Webhook ingestion + HMAC verification; CDAROwns
The authoritative AFNOR legal lifecycle statusProjects onlyOwns
Routing via the Annuaire (B2Brouter’s job once registered)Owns

3.1 The event contract between them

The two crates communicate over the eventbus using the authoritative contract in PA 10. Integration Contracts §6. Events carry ids and references, never aggregates or document bytes. The status field on any event is always the AFNOR legal layer — never a B2Brouter API status or an invented enum.

EventDirectionPurpose
InvoiceFinalizedinvoicingplateforme_agreeeA finalized, immutable AR invoice is ready for transmission. Carries invoice_id, organisation_id, and a reference to the structured invoice (PA 4). PA begins the outbound contract.
TransmissionStatusChangedplateforme_agreeeinvoicingAn authoritative AFNOR legal status changed. Carries transmission_id, invoice_id, legal_status, changed_at, optional reason. invoicing updates its projection.
TransactionMatchedinvoicingplateforme_agreeeA bank transaction settled an issued invoice in full — drives the AR commercial lifecycle to Paid. Settlement only: it does not by itself feed payment-data e-reporting. See 9. Transaction Matching.
PaymentCollectedinvoicingplateforme_agreeeA real bank-level VAT-collection event (full / partial / refund / overpayment / correction) was allocated to a document, writing one payment-allocation / VAT-collection ledger entry. It is the internal data source for payment data when VAT is on encaissement; the PA carries it as Enriched212Men for invoiced Flux-1 operations and Flux10 only for non-invoiced. Does not change the AFNOR legal status by itself.
InboundInvoiceReceivedplateforme_agreeebillsA supplier e-invoice arrived. AP-only — consumed by bills, never by invoicing. Listed here only to mark the boundary.
flowchart LR
    subgraph INV["invoicing (AR)"]
        Classical["Classical invoice
(author / edit)"] Finalize["Finalize
gap-free number + lock"] Structured["EN 16931
structured model"] Proj["Legal status
(projection)"] end subgraph PA["plateforme_agreee (transport)"] Trans["Transmission record
+ authoritative legal status"] end B2B["B2Brouter (PA)"] Classical --> Finalize --> Structured Finalize -->|InvoiceFinalized| Trans Trans --> B2B B2B -->|webhook / CDAR| Trans Trans -->|TransmissionStatusChanged| Proj Proj -.-> Classical

Design rule: The legal lifecycle status flows one direction for authority: plateforme_agreeeinvoicing. invoicing never writes the AFNOR status itself; it mirrors what TransmissionStatusChanged tells it. The full 3-layer mapping (AFNOR ↔ B2Brouter API ↔ Green-Got internal) lives in PA 5. Lifecycle Statuses and is not redefined in this crate.

Invariant: One AR invoice maps to one outbound transmission. Re-submission after a Rejetée correction issues a new transmission referencing the corrected invoice; it never mutates the prior one. See PA 10 §3.1.

4. Out of Scope

Out of scope for invoicing:

5. The Invoicing Doc Set

DocTopic
1. Invoicing OverviewThis document — AR scope, relationship to the transport layer.
2. Dual Invoice ModelThe two coupled representations: classical internal model + EN 16931 structured model, and how one derives the other.
3. Invoice DomainThe Invoice aggregate, lifecycle/state machine, finalization, immutability, and credit notes (avoir).
4. Quote DomainThe Quote aggregate, its lifecycle, conversion to invoice.
5. NumberingGap-free, sequential per-issuer document numbering.
6. Quote SignatureE-signature capture for quotes.
7. RemindersManual and automated reminder cadences.
8. Payment Link and CollectionPayment links on Green-Got rails; collection.
9. Transaction MatchingAR ↔ bank-transaction matching; full-amount settlement (TransactionMatchedPaid) and the VAT-collection ledger (PaymentCollected) that sources payment data (Enriched212Men for invoiced, Flux10 for non-invoiced).
10. Tax and VATVAT category codes, rates, multi-currency, debits vs encaissement.

6. Related Documents

10. Tax and VAT

Tax and VAT

This document is the target-design specification for VAT computation on issued invoices: the French VAT rates, the EN 16931 VAT category codes the invoice must carry, the reverse-charge / intra-EU / export cases, the VAT-on-debits vs VAT-on-collection regimes, and the totals computation (net HT → VAT per category → gross TTC) in integer cents.

1. Terminology

Terms shared across the documentation set are defined in the invoicing overview. This document adds:

2. French VAT Rates

RateNameTypical application
20 %Taux normal (standard)Default rate for most goods and services.
10 %Taux intermédiaireRestaurants, transport, certain renovation works, etc.
5.5 %Taux réduitFood staples, books, certain energy, etc.
2.1 %Taux particulier / super-réduitPress, reimbursable medicines, certain specific cases.
0 %Zero / exempt / out-of-scope contextCarried with the appropriate category code (Z / E / AE / K / G / O — see §3). A 0 rate is never stated without its qualifying category code.

Design rule: A VAT rate is always paired with a VAT category code. A 0 rate is meaningless on its own; it must be qualified as zero-rated (Z), exempt (E), reverse charge (AE), intra-EU (K), export (G), or out of scope (O). The rate is a number; the category explains it.

2.1 Organisation-level VAT defaults

VAT defaults are configured by the customer at the organisation settings level:

Design rule — org default applies, line may override. By default the organisation’s configured VAT (rate + exemption status) applies to every line of every invoice. Each invoice line may override with a custom VAT rate and category (§3); when a line does not override, the org default applies. The org setting is the default, never an authoritative per-line value.

3. VAT Category Codes (EN 16931)

The VAT category code (UNCL5305, EN 16931 BT-151 at line level / BT-118 at breakdown level) qualifies each taxed line. It is defined canonically in PA 4. Formats and Invoice Data §5; this document does not redefine it. Summary for reference:

CodeMeaningLine rate
SStandard rate (incl. reduced positive rates)Positive (20 / 10 / 5.5 / 2.1)
ZZero rated0
EExempt (with statutory reference)0
AEVAT reverse charge (autoliquidation)0
KIntra-community supply of goods0
GFree export, tax not charged0
OServices outside the scope of VAT0

Design rule: Per PA 4 §5, the category must be consistent with the goods-vs-services composition and the rate. S (and Z) carry a non-negative numeric rate; AE, K, G, E, O carry rate 0 and the breakdown must state the exemption / reverse-charge reason. France’s reduced rates (10 / 5.5 / 2.1) are still category S — they are reduced standard rates, not separate categories.

4. Reverse Charge, Intra-EU, and Export Cases

These are the non-standard category cases an issued invoice must encode correctly. In all three the seller charges 0 VAT, but for different legal reasons and with different mandatory mentions.

CaseCategoryWho accounts for VATMandatory mentionE-invoicing / e-reporting
Reverse charge (autoliquidation)AEThe buyer“Autoliquidation” mention; the buyer’s VAT numberDomestic B2B reverse charge stays on Flux 1; the seller reports no payment data (PA 7 §5).
Intra-EU supply of goodsKThe acquirer in the destination Member StateExemption reference (e.g. CGI art. 262 ter I); both VAT numbersCross-border → out of e-invoicing scope, in e-reporting scope (Flux 10 transaction data).
Export (outside the EU)GNot charged (export)Export exemption referenceCross-border → out of e-invoicing scope, e-reporting (Flux 10) applies.

Design rule: Cross-border B2B (intra-EU K, export G) is excluded from domestic e-invoicing (Flux 1) because the foreign counterparty is not reachable through the French Annuaire, and is instead e-reported over Flux 10 as cross-border transaction data. Domestic reverse charge (AE) stays on Flux 1 but the seller emits no payment data. See PA 7. E-Reporting §3.

Design rule — channel classification gates party identity and the buyer SIREN. Before finalization the operation is classified deterministically by channel from (Client.party variant, billing_address.country). The MVP supports two channels — domestic FR B2B → Flux 1 and foreign B2B + B2C → Flux 10; B2G (PublicEntity/Chorus Pro) and Monaco are out of MVP scope and are rejected/parked at finalize. Party identity is conditional by channelFrenchCompany, ForeignCompany, Individual, or PublicEntity. Domestic reverse-charge (AE) stays on Flux 1. A buyer SIREN is mandatory only for domestic FR B2B (the Annuaire routing key, 9-digit + Luhn-valid); it is not required for a B2C Individual or a ForeignCompany. The full deterministic algorithm and the MVP scope decision live in 2. Dual Invoice Model §3.5.

5. VAT on Debits vs VAT on Collection

French VAT has two chargeability regimes, and the invoice must state which applies. This is a mandatory mention of the reform (PA 4 §4).

RegimeVAT chargeable whenApplies toPayment-data e-reporting?
VAT on debits (sur les débits)The invoice is issued (the debit)Goods lines (mandatory); service lines whose provider elected débitsNo payment data — chargeable event is issuance, not collection.
VAT on collection (sur les encaissements)Payment is collectedService lines (default); never goodsYes — a payment-data overlay is derived from the PaymentCollected VAT-collection ledger entry; the carrier is the enriched 212/MEN for invoiced Flux-1 operations, and Flux 10 only for non-invoiced operations (D-CARRIER) (PA 7 §3.3, PA 7 §6).

Design rule — the election is one-way (services may opt for débits; goods cannot opt for encaissement). A service provider may elect the debits regime via the explicit “option pour le paiement de la taxe d’après les débits” mention. There is no symmetric option: a goods line is always on débits and can never elect VAT-on-collection. The invoice must carry the regime (and the option mention when elected) as structured data, not free text.

Design rule — chargeability is line-level; doc-level flags are derived. Both the goods-vs-services qualification and the VAT-chargeability regime (debits vs encaissement) are stored at line level. A goods line is intrinsically on débits; a service line defaults to encaissement and may be set to débits under the option. A mixed invoice can therefore carry a goods line on débits and a service line on encaissement in the same document. In practice almost all lines share the same value, but the model must allow per-line differences.

The document-level flags are derived from the per-line flags, never authored independently:

Invariant — no goods-on-encaissement. A line flagged goods must carry the débits regime. A configuration that puts a goods line on encaissement is invalid and is rejected at finalize (3. Invoice Domain §4 step 1). The chargeability that governs the payment-data overlay for a given collection is the one carried by the lines being collected (8. Payment Link and Collection §5): a mixed invoice’s collection reports payment data only for the encaissement-regime (service) lines.

Design rule — partial reportability is carried by the canonical ledger’s per-{rate, chargeability} breakdown groups; invoicing does NOT redefine it. A mixed invoice (some lines on débits, some on encaissement) at the same VAT rate must NOT over-report the débits (goods) VAT into the payment-data overlay. This is solved on the canonical payment-allocation ledger, not here: its vat_breakdown is grouped by both { vat_rate, vat_chargeability } and each group carries its own reportability_state, so a service group and a goods group at 20 % stay distinct and only the encaissement group is fed to the payment-data overlay (the enriched 212/MEN for this invoiced Flux-1 op; Flux 10 only for non-invoiced ops, D-CARRIER). The authoritative shape, the per-group reportability_state, and the worked example are defined once in PA 10 §11.1; invoicing defers to it and never redefines the ledger.

Worked example — mixed invoice, same rate, partial reportability. A finalized invoice has two lines, both at 20 %: goods €1,000 HT (VAT €200, vat_chargeability = débits) and services €500 HT (VAT €100, vat_chargeability = encaissement). The buyer pays the full €1,800 TTC in one transfer; one PaymentCollected ledger entry is appended with a two-group vat_breakdown:

Groupvat_ratevat_chargeabilitytaxable_basevat_amountreportability_state
Goods20 %débits€1,000€200not_reportable
Services20 %encaissement€500€100reportable

plateforme_agreee reports only the services group (€500 base / €100 VAT) over the channel-specific payment-data overlay (D-CARRIER) — the enriched 212/MEN for this invoiced Flux-1 operation, never a separate Flux 10 submission; the goods €200 VAT was already chargeable at issuance (CGI art. 269) and is excluded. A single-reportability_state-per-entry shape would have collapsed both into one {20 %} group and over-reported the goods VAT — which is exactly why the canonical ledger groups by {rate, chargeability} (PA 10 §11.1).

Design rule: The chargeability regime on the canonical structured invoice governs whether a confirmed collection produces a payment-data overlayPaymentCollected is the data source; the carrier is the enriched 212/MEN for invoiced Flux-1 operations, and Flux 10 only for non-invoiced operations (D-CARRIER). A real bank-level collection (9. Transaction Matching) emits PaymentCollected, writing a VAT-collection ledger entry; under encaissement that entry is the source from which the payment-data overlay is derived (for a domestic B2B invoiced op, carried by the enriched 212/MEN, not a separate Flux 10 submission). Settlement (TransactionMatched → AR-commercial Paid) and the AFNOR Encaissée projection are separate and are never the trigger for the reporting. Under débits (or reverse charge, or B2C already in B2C data), the same collection still emits PaymentCollected but produces no payment-data overlay. See 8. Payment Link and Collection §5, PA 7 §6 and PA 7 §5.

6. Totals Computation

Totals are computed bottom-up from line items, grouped into a per-category/per-rate VAT breakdown, and summed to the grand total. All amounts are integer cents (i64).

6.1 Computation order

  1. Per line — line net (HT) = quantity × unit_price_net, in integer cents. Each line carries its VAT category code and rate (§3).
  2. Per (category, rate) group — taxable base = sum of line nets in that group; VAT amount = round(base × rate). This is the EN 16931 VAT breakdown (BG-23). VAT is computed per group, not per line, so rounding happens once per rate, not once per line.
  3. Document totals
    • total net (HT) = sum of all line nets (BT-109);
    • total VAT = sum of per-group VAT amounts (BT-110);
    • total gross (TTC) = total net + total VAT (BT-112) = amount payable.

6.2 Rounding

Design rule: VAT is rounded per VAT breakdown group (per category/rate), once, to the cent — not per line and not only at the grand total. Total VAT is the sum of the already-rounded per-group amounts, so the printed breakdown reconciles exactly to the printed total VAT. All arithmetic is in integer cents; no floating-point money.

6.3 Worked example

Three lines, mixed rates, EUR (amounts in cents shown as € for readability):

LineDescriptionQtyUnit price HTLine net HTCategoryRate
1Consulting (service)10€100.00€1,000.00S20 %
2Printed book4€25.00€100.00S5.5 %
3Intra-EU goods supply1€500.00€500.00K0 %

VAT breakdown (per category/rate group):

GroupTaxable base HTRateVAT (rounded per group)
S @ 20 %€1,000.0020 %€200.00
S @ 5.5 %€100.005.5 %€5.50
K @ 0 %€500.000 %€0.00 (intra-EU; acquirer accounts)

Document totals:

TotalAmount
Total net (HT)€1,600.00
Total VAT€205.50
Total gross (TTC)€1,805.50

The €500 intra-EU line is in the breakdown at category K, rate 0, with the exemption reference; it contributes to total net but adds no VAT, and (being cross-border) is e-reported over Flux 10 rather than carried as domestic B2B e-invoicing.

7. Currency

One currency per invoice; there are no mixed-currency lines — every line and total on an invoice is denominated in the single invoice currency (BT-5). For now Green-Got issues EUR-only (domestic French B2B is in EUR).

Design rule — currency-mismatch is rejected, never auto-converted. A settling match requires the inbound bank transaction’s currency to equal the invoice currency. A transaction in a different currency is rejected as an exact match and surfaced as an anomaly to the org user (9. Transaction Matching §8); the system never silently FX-converts to force a match. While invoices are EUR-only this is moot, but the rule is the guard that makes future multi-currency safe.

Design rule — future FX-snapshot rule (when multi-currency lands). When per-invoice multi-currency is later enabled, a non-EUR invoice that is collected will snapshot the FX rate at collection date on the PaymentCollected ledger entry (the entry already carries currency), and the payment-data overlay carries the original currency + amount (on the enriched 212/MEN for an invoiced Flux-1 op, on Flux 10 only for a non-invoiced op, D-CARRIER); no EUR-equivalent is stored on the invoice itself. Until then, no EUR-equivalent amount is stored alongside the invoice currency (no FX-rate snapshotting), and mixed-currency lines within one invoice are never allowed.

8. Invariants

9. Related Documents

10. Sources

11. API Contract

API Contract

This document is the target-design specification for the domain-to-API contract between the invoicing AR domain and the business_api HTTP/RPC surface: the draft vs finalized payloads, the typed legal-identity fields the current thin DTOs lack, immutable finalization fields, the status projection rules, and the path from today’s fixture-backed routes to a generated frontend SDK.

1. Terminology

Shared reform terms are deferred to the PA glossary (PA 4 §1, PA 5 Terminology) and the dual-model terms to 2. Dual Invoice Model §1. Local terms:

2. Why the Current DTOs Cannot Carry the Legal Model

The live business_api exposes thin, fixture-backed types: Invoice is { id, number, client_id, client_name, status, total_amount, currency, issued_at, due_at, created_at }, and the routes are served from fake_data.rs, not a persisted domain. This summary is correct for the list table but cannot represent a transmissible invoice.

The classical model requires far more (2. Dual Invoice Model §3.3, 3. Invoice Domain §2.1): line items with per-line VAT category and chargeability, buyer SIREN/SIRET as the routing key, a distinct delivery address when it differs from billing, structured payment means, document type, and the structured BT-21 legal notes. None of these exist on the thin Invoice DTO.

Design rule: The thin DTO is a list projection, not the invoice. The API must expose a richer draft/finalized payload for authoring and reading a single invoice, while the existing summary type stays the shape of list endpoints only. The summary is derived from the full model, never the other way round.

Design rule — the API is a projection of the domain, not its definition. The canonical model lives in the invoicing domain (2. Dual Invoice Model, 3. Invoice Domain); the business_api payloads are a serialisation of it. Field mandatory/optional status is governed by PA 4 §4 and the domain, not by what is convenient for the current front-end fixtures.

3. Draft Payload vs Finalized Payload

A single invoice has two payload shapes keyed off whether it is still editable. They are not the same type with optional fields — they encode different guarantees.

AspectDraft payload (status = Draft)Finalized payload (post-finalize)
MutabilityEvery field editable; the only state a document may be deleted from.Immutable; number, fields, and structured model locked (3. Invoice Domain §4).
numberAbsent (assigned at finalize).Present — gap-free (BT-1), assigned at finalize.
Mandatory EN 16931 termsMay be incomplete — the draft is a work in progress.All present — finalize fails if any mandatory term cannot be populated.
Structured modelNot generated.Generated and locked; the transmission source of truth.
TotalsDisplay totals, recomputable.Recomputed authoritatively at finalize; authoritative over display.
CorrectionsEdit in place.Never edited — corrected only by an avoir (type 381) (3. Invoice Domain §5).

Design rule — finalize is a distinct API action, not a status PATCH. Finalization runs validation, gap-free numbering, structured-model generation, and locking (3. Invoice Domain §4). It is a dedicated endpoint (e.g. invoicing.invoices.finalize) that fails if any mandatory term is missing — never a client-supplied transition of the status field. The client cannot set status = Pending/Settled directly; those are server-derived.

Design rule — the draft validates lazily, finalize validates strictly. A draft accepts partial data so the operator can author incrementally. The finalize call applies the full PA 4 §4 check and returns structured per-field errors so the UI can surface exactly what blocks transmission. The required set is conditional on the channel classified before finalization: a buyer SIREN/SIRET is required only for domestic FR B2B (FrenchCompany, Flux 1) — not for a B2C Individual or a ForeignCompany (Flux 10), nor a PublicEntity (B2G / Chorus Pro). Other checks (missing delivery address when ≠ billing, unset per-line VAT category, …) apply per their own conditions. Party identity is conditional by channel — see 2. Dual Invoice Model §3.3 and 10. Tax and VAT §4.

4. Typed Legal-Identity Fields

The contract must carry French/EU legal identifiers as typed, validated fields, not bare strings. The current DTOs already model siren/nic/vat_number as String on ClientType::Company; the target contract keeps them structured and validated.

FieldType / shapeValidationEN 16931 / source
Buyer SIREN9-digit identifierlength + Luhn check; resolvable in the Annuaire. Required only for domestic FR B2B (FrenchCompany); absent for Individual (B2C) and ForeignCompany (Flux 10)BT-47 routing key (2. Dual Invoice Model §3.3).
Buyer SIRETSIREN (9) + NIC (5)NIC validity; establishment designationBT-47 variant (specific establishment).
EU VAT numbercountry prefix + bodyintra-community format per countryBT-48 (buyer), BT-31 (seller).
Address{ line1, line2?, zip_code, city, country (ISO 3166-1 a2), state? }mandatory country; structured, not a free-text blockBG-8 (billing), BG-15 / BT-75…BT-80 (delivery).
Legal formenum/string (SAS, SARL, EI, …)from a known set where applicableseller/buyer legal block.
CurrencyISO 4217one per document; EUR-only for nowBT-5 (10. Tax and VAT §7).

Design rule — legal identifiers are typed, validated, and self-describing. SIREN/SIRET/VAT/country are not interchangeable opaque strings. The contract carries each as its own field with its own validation, so the generated SDK is type-safe and the front-end cannot, e.g., submit a SIRET where a SIREN is expected. Server-side validation is authoritative; client-side validation is a UX convenience.

Design rule — the buyer identity lives on the Client, referenced by the invoice. The invoice payload carries client_id and a denormalised display name; the typed SIREN/SIRET/VAT/address fields are authored once on the Client master (2. Dual Invoice Model §3.1) and resolved at finalize. A per-invoice delivery_address overrides the client default (3. Invoice Domain §2.1).

5. Per-Line Tax and Structured Legal Notes (BT-21)

The thin DTO has no line items at all. The draft/finalized payload must carry them, each line with its own tax fields, and must carry the structured legal notes as typed fields.

5.1 Line items

Each line carries: description (BT-153), quantity (BT-129), unit price net (BT-146), line net (BT-131), VAT category code (UNCL5305, BT-151) + rate (BT-152), the per-line goods_or_service flag, and the per-line vat_chargeability (debits | encaissement). VAT regime and goods/services qualification are per line, with the document-level values derived (3. Invoice Domain §2.1, 10. Tax and VAT §5).

Design rule — tax is a per-line field, not a document field, in the payload. The contract carries vat_category, rate, goods_or_service, and vat_chargeability on each line. The document-level vat_chargeability (debits | encaissement | mixed) and goods_services_composition are read-only derived fields in the response — the API never accepts them as writable inputs.

5.2 Structured legal notes (BT-21)

The mandatory B2B legal mentions — recovery fee (indemnité forfaitaire), late-payment penalties (pénalités de retard), and escompte terms — are carried as structured { subject_code, text } fields with a UNCL4451 subject code (PMT / PMD / AAB), not free-text footer lines (2. Dual Invoice Model §3.4).

Design rule — structured notes are typed, free-text notes are not. The BT-21 legal notes are first-class { subject_code, text } fields in the payload (subject code from the UNCL4451 set), defaulted from the organisation profile and renderable to both the PDF face and the XML. The classical-only header/footer/branding notes remain free-text and carry no subject code; the contract keeps the two distinct so a free-text note can never be mistaken for a compliant legal mention.

6. Immutable Finalization Fields

After finalize, a subset of the payload is locked and the contract must mark it so. The generated SDK and the front-end must treat these as read-only on a finalized invoice.

Locked at finalizeWhy
number (BT-1)Gap-free sequence; a renumbered/deleted entry breaks the audit trail (5. Numbering).
Line items, totals, tax breakdownFrozen into the structured model; recomputed authoritatively at finalize.
Buyer identity snapshot, addresses, payment meansThe transmitted legal record; corrections issue an avoir.
Structured legal notes (BT-21)Part of the locked structured model.
document_type (380)Fixed for the aggregate.

Design rule — the API rejects mutation of a finalized invoice. Any write to a finalized invoice’s locked fields is rejected (no in-place edit, no delete). The only mutation path on a finalized invoice is issuing an avoir (3. Invoice Domain §5), which is a separate document with its own create/finalize endpoints, never a PATCH of the original. Only a Draft may be deleted.

Design rule — finalized fields are read-only in the schema, not merely by convention. The contract distinguishes the editable draft fields from the locked finalized fields at the type level (e.g. a FinalizedInvoice response whose locked fields have no corresponding update endpoint), so the generated SDK encodes immutability rather than relying on runtime 4xx alone.

7. Status Projection Rules

The API exposes the coarse business_api status and decorates it with the reflected AFNOR legal status. It never exposes the persisted canonical domain status as the writable field.

business_api statusReached when (canonical internal)Reflected AFNOR legal status
DraftDraft(none — pre-legal)
PendingIssuedPaymentTransmitted (all transmitted/in-flight states)the precise code 200–211, projected
SettledSettledEncaissée (212) (reflected)
CanceledRefused / Rejected / CancelledRefusée (210) / Rejetée (213) / Annulée

Design rule — the API status is a projection, the canonical status is persisted. The business_api set is presentation-only and lossy on purpose; the domain MUST persist the full canonical AFNOR-aligned set so the mandatory legal statuses stay derivable (3. Invoice Domain §3.1, PA 5 §9). The API surfaces both the coarse label and the reflected AFNOR code; it never lets the client write either.

Design rule — Settled is server-derived from collection, not client-set. The transition to Settled is driven by transaction matching (9. Transaction Matching), and the reflected Encaissée legal status is a projection from the PA. Neither is a writable API field. Payment-data reporting is fed by the separate payment-collection event, not by setting a status (9. Transaction Matching §6.1).

Design rule — the status field is FILTER-ONLY on the API surface, never a write target. On list/query endpoints the coarse business_api status (Draft | Pending | Settled | Canceled) may be supplied as a read filter (e.g. “list Pending invoices”). It is never accepted as a write/update field on any create, update, or PATCH payload: status transitions happen only through the dedicated lifecycle actions (finalize, send, mark_paid, void, …) and projections from the PA, never by a client writing the status field. The generated SDK must model status as read-only on entity payloads and as an optional query parameter on list endpoints — the two uses are kept distinct so a client cannot smuggle a state change through a “filter”.

8. Frontend Generated-SDK Migration Path

Today the front-end is served by fixture-backed routes (fake_data.rs) and front-end-defined types. The target is a generated SDK built from the API’s utoipa/OpenAPI schema, so the contract is single-sourced from the Rust DTOs.

The migration is staged so the front-end is never broken:

  1. Lift the schema. Define the full draft/finalized payloads, typed legal fields, line items, and BT-21 notes as utoipa::ToSchema DTOs in business_api, replacing the front-end’s hand-written invoice types as the source of truth.
  2. Generate the SDK. Produce the typed client from the OpenAPI document; the front-end imports generated types instead of locally-defined ones. The thin Invoice summary remains the list type; the new rich payloads back the single-invoice authoring/detail views.
  3. Swap fixtures for the domain. Replace fake_data.rs responses with the persisted invoicing domain behind the same generated contract, route by route, so each endpoint flips from fixture to real without a schema change.
  4. Add the write surface. Introduce the draft create/update, finalize, and avoir endpoints (absent today — the live router is read-only plus client CRUD), each validating against the domain.

Design rule — the Rust DTO is the single source of truth for the contract. The OpenAPI schema is generated from the business_api ToSchema types, and the front-end SDK is generated from that schema. The front-end never re-declares invoice shapes; a contract change is made once in Rust and flows to the SDK. This removes the drift between front-end fixtures and the legal model that the thin DTOs invite.

Design rule — migrate behind a stable contract, not a big-bang rewrite. The schema is defined first and the fixture→domain swap happens endpoint by endpoint behind it, so the generated SDK is stable while the backing implementation moves from fixtures to the persisted AR domain. List endpoints keep their summary shape throughout.

Design rule — invoicing is EXCLUDED from the STABLE published business OpenAPI until the rich DTOs land. The fixture-backed thin DTOs (§2) cannot represent a transmissible invoice, so they MUST NOT be the basis of a stable, published client contract. Until the rich draft/finalized payloads, typed legal-identity fields (§4), per-line tax + BT-21 notes (§5), and typed legal validation at finalize (§3) are implemented, the invoicing routes are namespaced out of / excluded from the STABLE published business OpenAPI — equivalently, the generated SDK is flagged UNSTABLE for invoicing. Business clients must not generate stable SDKs against the fixture DTOs: a stable SDK built on the thin shape would bake in a contract that cannot carry the legal model and would break the moment the rich DTOs replace it.

Design rule — add a read-only reflected_legal_status to the single-invoice payload when rich DTOs land. When the rich draft/finalized payloads are introduced (§8 step 1), the single-invoice (detail) payload gains a read-only reflected_legal_status field carrying the AFNOR XP Z12-012 code (200–213, or Annulée) the invoice projects from plateforme_agreee (3. Invoice Domain §6), alongside the coarse commercial status. It is decorative/read-only — surfaced so the UI can show the precise legal stage — and is never writable. The thin list summary keeps the coarse status only; the reflected legal code belongs on the single-invoice detail payload. This is tracked as part of the migration gate below.

Migration gate — when invoicing joins the stable SDK. Invoicing routes graduate into the STABLE published business OpenAPI (UNSTABLE flag removed) only once all of: (1) the rich draft/finalized DTOs are defined as utoipa::ToSchema types and are the source of truth (§8 step 1), (2) the typed legal-identity fields and per-line tax + BT-21 notes are modelled (§4, §5), (3) finalize performs strict typed legal validation returning structured per-field errors (§3), (4) the immutable-finalization field set is encoded at the type level (§6), and (5) the single-invoice detail payload carries the read-only reflected_legal_status field (above) — are in place. Until that gate is met, invoicing stays namespaced as UNSTABLE; the thin list summary may remain published as a list projection, but the single-invoice authoring/detail contract is not stable.

9. Invariants

10. Related Documents

2. Dual Invoice Model

Dual Invoice Model

This document defines the two coupled representations every Green-Got invoice carries — a classical internal model for authoring, display, and editing, and an EN 16931 structured model for regulated e-invoice transmission — and the mapping by which the structured model is generated from the classical one at finalize time.

1. Terminology

Shared reform terms (EN 16931, Factur-X, SIREN, SIRET, NIC, UNCL5305, UNCL1001, VAT on debits/encaissement, …) are defined in the PA glossary and in PA 4. Formats and Invoice Data §1; this doc does not redefine them. Local terms:

2. Why Two Models

The reform’s purpose is structured data: a free-text rendering of a mandatory field is non-compliant if the corresponding structured EN 16931 term is absent (PA 4 §4). At the same time, an operator authors and reads an invoice as a document — a branded page with a client, line items, and notes — not as a tree of BT-n terms.

Green-Got therefore keeps both:

Design rule: The two models are coupled but not symmetric. The classical model is the editing surface; the structured model is the transmission source of truth. They are reconciled by a deterministic derivation, not maintained independently.

3. The Classical Internal Model

The classical model reflects the current business_api shape (see the routes/invoicing/models.rs Client / Invoice types). It is human-facing and edit-oriented.

3.1 Client (the buyer party master)

A Client (“cli_…”) is the AR counterparty. It is authored once and reused across that buyer’s documents.

FieldTypeNotes
partyClientTypeOne of FrenchCompany { legal_name, trade_name?, siren(9), nic(5), vat_number?, legal_form? }, ForeignCompany { legal_name, trade_name?, country, vat_number? }, Individual { first_name, last_name }, or PublicEntity { legal_name, siren(9)?, service_code? } (B2G). SIRET = siren + nic. The FR-vs-foreign discriminator is derived (see §3.5 channel classification), and the party variant is what carries it on the master.
email, phoneoptionalPrimary contact channels.
billing_addressAddressEN 16931 BG-8: line1, line2?, zip_code, city, country (ISO 3166-1 a2), state?.
delivery_addressoptional AddressDefault distinct delivery / lieu de livraison. An invoice may override it with its own delivery_address (see 3. Invoice Domain §2.1).
buyer_referenceoptionalThe buyer’s PO / reference.
document_languageISO 639-1Default language for rendered documents.
document_currencyISO 4217Default currency.
default_payment_terms_daysoptionalDefault settlement window.
additional_contacts_count, contactsExtra Contact records.
outstanding_amount, collected_amountcentsDenormalised AR rollups.
archived, timestamps

3.2 Invoice (the document)

The classical invoice (“inv_…”) is what the operator edits. The current business_api projection exposes a coarse summary (number, client_id, client_name, status, total_amount, currency, issued_at, due_at, created_at); the full classical model additionally carries:

Field groupContentPurpose
Headernumber (assigned at finalize), client reference, issue date, due date, document language/currencyThe invoice identity and settlement terms.
Line itemsper line: description, quantity, unit price (net), VAT rate, VAT category, line netWhat is being billed. The editable list.
Notesfree-text header/footer notes, terms text, plus the structured legal notes (§3.4)Free-text annotations are classical-only; the structured legal notes (recovery fee / penalties / escompte) carry a UNCL4451 subject code and render to the XML too.
Brandinglogo, colours, template choiceThe rendered look of the document (PDF face).
Display totalstotal net, total VAT (per rate), total grossComputed for display; recomputed authoritatively into the structured model at finalize.
Statusthe internal lifecycle status + projected legal statusSee 3. Invoice Domain.

Design rule: Branding, notes, and display formatting live only in the classical model. They are not EN 16931 terms and are never required by the structured model (they may appear in the human-readable Factur-X PDF face, but they are not part of the structured contract).

3.3 Fields the classical model must additionally capture to be transmissible

The current business_api invoice summary is not sufficient to generate a compliant structured invoice. To be transmissible, the classical model must capture every mandatory EN 16931 term from PA 4 §4 as a first-class field — not a free-text mention. The fields the classical model must add beyond the simple summary are:

3.4 Structured legal notes (BT-21)

French B2B invoices must carry several legal mentions — the indemnité forfaitaire de recouvrement (40 €), the pénalités de retard, and the escompte (discount-for-early-payment) terms. The reform expresses these as structured invoice notes with a subject code (EN 16931 BT-21, code list UNCL4451), not as anonymous free-text footer lines. A free-text rendering alone is non-compliant when a structured subject code exists (§2).

The classical model therefore carries these as first-class structured fields, each with its UNCL4451 subject code, so the same source field renders into both the PDF face and the structured XML. The canonical code list is defined in PA 4 §4.2; this crate populates it.

Legal note (classical field)UNCL4451 subject codeTypical contentDefault source
Recovery fee — indemnité forfaitaire de recouvrementPMT“Indemnité forfaitaire pour frais de recouvrement : 40 €”Organisation default (fixed legal 40 €).
Late-payment penalties — pénalités de retardPMDPenalty rate / terms applicable on late paymentOrganisation default (configurable rate/terms).
Discount / escompte termsAAB“Escompte pour paiement anticipé : …” or “Pas d’escompte pour paiement anticipé”Organisation default; per-invoice override allowed.

Design rule — organisation-level defaults. The recovery-fee, late-penalty, and escompte notes are configured once per organisation and applied to every issued invoice by default. An invoice may override the escompte terms for that document (e.g. a negotiated discount); the recovery-fee mention is the fixed statutory 40 € and is not per-invoice editable. The defaults live in the organisation profile, not re-authored per draft.

Design rule — one source, two renderings. Each structured legal note is stored once as a { subject_code, text } pair on the classical model. At finalize it renders into both the human-readable PDF face and the EN 16931 BT-21 structured note in the XML — never two divergent copies. The PDF face and the structured note cannot disagree because they derive from the same field, exactly as the §7 consistency rule requires for the rest of the document.

3.5 Channel classification and the deterministic finalize algorithm

Channel determination must be deterministic — the SIREN-mandatory check at finalize (§6) cannot run without first knowing the channel, and the channel selects the transport (Flux 1 routing vs Flux 10 e-reporting). It is computed from the Client.party variant + billing_address.country, never authored as a free field.

MVP scope decision — B2G and Monaco are OUT of MVP. The MVP supports exactly two channels: Flux 1 (domestic FR B2B) and Flux 10 (foreign B2B + B2C e-reporting). PublicEntity (B2G / Chorus Pro) and Monaco-routed operations are explicitly out of scope for the MVP; the PublicEntity variant exists on the master only so the model need not change later, but finalize rejects a draft whose classified channel is B2G or Monaco with a “channel out of MVP scope” error (it is parked, not transmitted). No B2G / Chorus Pro transmission path, event, or wiring is in MVP scope. The canonical multi-channel routing rule (including B2G and Monaco) lives in PA 10 §3.1 / PA 2; invoicing defers to it when those channels are later brought in.

Deterministic classification (evaluated at finalize, in order):

  1. party == PublicEntityB2Gout of MVP (finalize rejects/parks).
  2. party == FrenchCompany and billing_address.country == "FR"domestic FR B2B → Flux 1. A buyer SIREN is mandatory here (the Annuaire routing key). Domestic reverse-charge (category AE) stays on Flux 1 — the channel is unchanged by the VAT category; only the seller’s payment-data obligation differs (10. Tax and VAT §4).
  3. party == FrenchCompany and billing_address.country == "MC" (Monaco) → treated as domestic in principle, but out of MVP (finalize rejects/parks).
  4. party == ForeignCompany (any non-FR billing_address.country) → cross-border B2B → Flux 10 (e-reporting; no Annuaire routing, no buyer SIREN).
  5. party == IndividualB2C → Flux 10 (no buyer SIREN).

SIREN validity (domestic FR B2B only). Where a buyer SIREN is mandatory (case 2), it must be a 9-digit identifier whose Luhn (modulo-10) checksum is valid — the SIREN is valid iff the Luhn sum of its 9 digits is a multiple of 10. This is a format/checksum gate only; it detects transposition/typing errors but does not by itself prove the SIREN exists in the INSEE register or is reachable in the Annuaire (resolvability is a separate finalize check). A SIRET is SIREN(9) + NIC(5); the NIC carries its own Luhn-consistent control. See 11. API Contract §4.

Design rule — channel is derived, SIREN-mandatory follows from it. The channel is a pure function of (party variant, billing_address.country) and is recomputed at finalize, never stored as an editable field. The “buyer SIREN mandatory” rule is scoped to the domestic-FR-B2B channel only (case 2); finalize never requires a SIREN for Flux 10 channels, and rejects (parks) the out-of-MVP B2G / Monaco channels.

4. The EN 16931 Structured Model — Defined by Reference

The structured model is not redefined here. Its canonical field list — mandatory mentions, the EN 16931 BG-n/BT-n term mapping, VAT category codes (UNCL5305), and document type codes (UNCL1001) — lives in PA 4. Formats and Invoice Data. That document governs which fields exist, which are mandatory, and what they mean. This crate populates it; it does not redefine it.

Invariant: A field’s mandatory/optional status is governed by PA 4, not by this doc or by the B2Brouter API surface. Green-Got validates its structured model against PA 4 before submission; B2Brouter’s XSD + Schematron validation is a second line of defence.

5. Mapping — Classical Field → EN 16931 Mandatory Mention

The table maps each classical field to the EN 16931 term it populates in the structured model. The EN 16931 term names and mandatory status are authoritative in PA 4 §4; this table is the derivation view. Rows marked (must add) are the fields the classical model must capture beyond the simple business_api summary (see §3.3).

Classical field (source)EN 16931 termMandatory?Note
Seller org siren (from organisation/enrollment)BT-30 (seller legal reg. id, scheme 0002)MandatoryThe customer’s own SIREN; resolved from the organisation, not authored per invoice.
Seller name / address / VAT number (organisation)BG-4, BT-31MandatoryFrom the organisation profile.
Client.party.sirenBT-47 (buyer legal reg. id)Mandatory for domestic FR B2B (must add)Routing key via the Annuaire. Required only when the buyer is a FrenchCompany (Flux 1); not required for Individual (B2C) or ForeignCompany (Flux 10).
Client.party SIRET (siren+nic)BT-47 variantConditionalWhen the buyer designates a specific establishment.
Client name / billing_address / vat_numberBG-7, BT-48MandatoryBuyer party block.
Client.delivery_address (or per-invoice override)BG-15 (BT-75…BT-80)Mandatory when ≠ billing (must add)Distinct delivery address.
Per-line goods/service flagline/document qualificationMandatory (must add)Goods-vs-services composition.
Document VAT-chargeability statementdocument-level statementMandatory when applicable (must add)Debits vs encaissement; option mention.
Per-line / breakdown VAT categoryBT-151 / BT-118 (UNCL5305)Mandatory (must add)Operation category for VAT.
Document type (380/381/…)BT-3 (UNCL1001)Mandatory (must add)380 invoice, 381 avoir.
Structured legal notes (recovery fee / penalties / escompte)BT-21 note + subject code (UNCL4451: PMT / PMD / AAB)Mandatory (must add)Organisation defaults; same source renders to PDF + XML. See §3.4.
Invoice number (finalize)BT-1MandatoryGap-free; see 5. Numbering.
issued_atBT-2MandatoryIssue date.
due_at / payment termsBT-20 / BT-9MandatorySettlement terms + due date.
Structured payment means (IBAN, means code)BG-16 (BT-81, BT-84…)Mandatory (must add)Metadata only; PA settles nothing.
Line: descriptionBT-153Mandatory
Line: quantityBT-129Mandatory
Line: unit price (net)BT-146Mandatory
Line: line net amountBT-131Mandatoryqty × unit price.
Line: VAT category + rateBT-151 / BT-152Mandatory
Display total netBT-109MandatoryRecomputed authoritatively at finalize.
VAT breakdown per rateBG-23 (BT-116…BT-119)MandatoryTaxable base + VAT per category/rate.
Display total VATBT-110Mandatory
Display total grossBT-112MandatoryAmount payable.
currencyBT-5MandatoryISO 4217; one currency per invoice, EUR-only for now (see 10 §7).
Free-text notes / branding / template(no EN 16931 term)Classical-only; appears on the PDF face, not in the structured contract. (The structured legal notes are the row above — they are EN 16931 BT-21 terms.)

6. Derivation — Classical → Structured at Finalize

The structured model is generated from the classical model at finalize time, not maintained in parallel. Finalization assigns the gap-free number, locks the editable fields, and produces the immutable structured invoice that is the transmission source of truth.

flowchart TD
    Draft["Classical model (Draft)
client · lines · notes · branding · display totals"] Edit{"Still a draft?"} Draft --> Edit Edit -->|yes| Draft Edit -->|finalize| Validate["Validate against PA 4
(all mandatory terms present)
+ channel classified (§3.5)
+ no goods line on encaissement
+ SIREN 9-digit Luhn-valid for FR B2B"] Validate -->|missing mandatory term / goods-on-encaissement / B2G or Monaco channel| Reject["Reject finalize
(non-transmissible / out of MVP scope)"] Validate -->|complete| Number["Assign gap-free number (BT-1)
see 5. Numbering"] Number --> Generate["Generate EN 16931 structured model
(canonical = PA 4)"] Generate --> Lock["Lock classical model (immutable)"] Lock --> Emit["Emit InvoiceFinalized
→ plateforme_agreee"] Emit --> SoT["Structured model = transmission source of truth"]

Design rule: The structured model is generated from the classical model at finalize time and is the transmission source of truth. While an invoice is a draft, only the classical model exists and is editable. Once finalized, the structured model is locked alongside the classical model and is what plateforme_agreee maps to B2Brouter JSON. Recomputed totals in the structured model are authoritative over the classical display totals if they ever diverge.

Invariant: Finalize fails if any mandatory EN 16931 term (PA 4 §4) cannot be populated from the classical model — most commonly a missing buyer SIREN, a missing delivery address when it differs from billing, or an unset VAT category. The Validate node also enforces three additional rejections, enumerated alongside the mandatory-term check (not separate from it): (a) no goods-on-encaissement — any line with goods_or_service == goods carrying vat_chargeability == encaissement rejects finalize (10. Tax and VAT §5/§8); (b) SIREN validity — for the domestic-FR-B2B channel the buyer SIREN must be 9-digit and Luhn-valid; (c) channel in MVP scope — a draft classified as B2G or Monaco (§3.5) is rejected/parked. A draft that cannot derive a complete structured model, or that fails any of these checks, is not transmissible and cannot be finalized.

7. Round-trip Considerations

8. Related Documents

3. Invoice Domain

Invoice Domain

This document defines the Invoice aggregate — its fields (including the e-invoice mandatory mentions), its canonical lifecycle state machine and the coarse business_api projection of it, finalization and immutability, and the credit-note (avoir, document type 381) model that corrects finalized invoices.

1. Terminology

Shared reform terms are deferred to the PA glossary (PA 4 §1, PA 5 Terminology). Local terms:

2. The Invoice Aggregate

An Invoice is an AR document a Green-Got customer issues to a buyer. It is exclusively AR — a received supplier invoice is never an Invoice (it is a Bill; see PA 10 §4).

2.1 Fields

The aggregate carries the classical model (2. Dual Invoice Model §3) plus the e-invoice mandatory fields required to derive a compliant structured model. Money is integer cents (i64); ids are prefixed time_sortable_id strings.

FieldTypeNotes
id“inv_…”Prefixed, time-sortable.
numberstring, optional until finalizeGap-free document number (BT-1), assigned at finalize. See 5. Numbering.
document_typeUNCL1001 = 380Commercial invoice. See PA 4 §6.
client_id“cli_…”The buyer party. Carries buyer SIREN/SIRET (mandatory routing key) and the billing/delivery addresses.
client_namestringDenormalised display name.
statuscanonical internal statusSee §3.
legal_statusAFNOR XP Z12-012, nullableReflected AFNOR legal status, projected from plateforme_agreee via TransmissionStatusChanged. See §6.
transmission_idFK to PaTransmission, nullableBack-reference to the plateforme_agreee transmission carrying the authoritative legal status. Owned by PA, referenced here.
line_items[]per lineDescription (BT-153), quantity (BT-129), unit price net (BT-146), line net (BT-131), VAT category (BT-151) + rate (BT-152), per-line goods_or_service flag and per-line vat_chargeability (debits | encaissement). VAT regime and the goods/services qualification are stored on the line, not the document. See 10. Tax and VAT §5.
delivery_addressAddress, nullablePer-invoice delivery address. When set, it overrides the Client’s default delivery_address; otherwise the Client default applies. Mandatory in the structured model when ≠ billing (BG-15).
vat_chargeability (document-level, derived)debits | encaissement | mixedDerived from the per-line vat_chargeability — a single regime when all lines agree, mixed when they differ. The per-line value is authoritative; this document-level field is a projection for the chargeability statement. Governs Flux 10 payment-data reporting per line collected. See 10. Tax and VAT §5.
goods_services_composition (derived)goods | services | mixedDerived from the per-line goods_or_service flags: goods/services when uniform, mixed when lines differ. Not authored independently. See 10. Tax and VAT §5.
payment_meansstructured (IBAN, means code)BG-16; metadata only.
totalstotal net (BT-109), VAT breakdown per rate (BG-23), total VAT (BT-110), total gross (BT-112)Recomputed authoritatively at finalize.
currencyISO 4217 (BT-5)One currency per invoice; EUR-only for now. Field retained for future per-invoice multi-currency. See 10 §7.
notes, brandingclassical-onlyNot EN 16931 terms; PDF face only.
issued_at, due_at, created_attimestampsIssue date (BT-2), due date (BT-9).

Design rule: Every mandatory EN 16931 term in PA 4 §4 is a first-class field on the aggregate, not a free-text note. The mandatory set the simple business_api summary omits — buyer SIREN/SIRET, distinct delivery address, VAT category code, operation category, VAT-on-debits option, structured payment means — is enumerated in 2. Dual Invoice Model §3.3.

Design rule — VAT regime and goods/services live on the line. The VAT-chargeability regime (vat_chargeability) and the goods-vs-services qualification (goods_or_service) are stored per line item, and the document-level vat_chargeability and goods_services_composition are derived projections of those per-line values, never independently authored. A goods line is always on débits and cannot elect VAT-on-collection; only a service line may opt for débits — a goods line on encaissement is rejected at finalize. The line-level model, the one-way election, and mixed goods/services behavior are specified in 10. Tax and VAT §5.

3. The Canonical State Machine

Design rule — two distinct status axes (DECISION D6). An invoice carries two orthogonal axes, persisted in two columns and never collapsed into one enum:

  1. AR-commercial status (status column, Green-Got-owned): Draft | Issued | Sent | Viewed | Overdue | Paid | Void. This is the axis the send_invoice, viewed-callback, mark_overdue, mark_paid, and void_invoice transitions drive. Sent, Viewed, and Overdue have no AFNOR equivalent (AFNOR has no “viewed” or “overdue” code); Paid is set by a full-amount match; Void is a commercial void, distinct from AFNOR Cancelled/Refused. Timestamp columns sent_at / viewed_at / paid_at record these transitions.
  2. AFNOR legal status (legal_status column, reflected, never authored by invoicing): the AFNOR XP Z12-012 set (200–213) plus the rare Cancelled, projected from plateforme_agreee via TransmissionStatusChanged. Settled (Encaissée 212) and Cancelled (Annulée) live strictly on this legal axis. The legal Settled is the legal-axis mirror of the commercial Paid: Green-Got’s collection drives both — mark_paid sets the commercial Paid, and the same collection is reflected back as AFNOR 212.

The two axes are correlated (a full-amount match advances the commercial axis to Paid and is mirrored by the reflected AFNOR Settled) but are persisted independently so the mandatory AFNOR statuses stay derivable from the legal axis while the commercial UI/dunning lifecycle runs on the commercial axis. The live business_api exposes a coarse projection over both axes (Draft | Pending | Settled | Canceled); the projection never replaces either persisted axis.

The diagram and §3.1 table below describe the AFNOR legal axis (the richer, regulated lifecycle the invoice reflects); the AR-commercial axis is the seven-state machine above (and in plan.md). Both are documented; neither projection replaces the persisted domain state.

The AFNOR legal axis the invoice reflects is fixed by PA 5 §9 and is a 1:1 image of the full AFNOR XP Z12-012 14-status set (codes 200–213) plus the rare Cancelled. The invoicing crate reflects 100% of the AFNOR statuses on the legal_status column (PA 5 §2 design rule); the pre-legal Draft/Issued and the commercial Sent/Viewed/Overdue/Paid/Void live on the commercial axis (D6, above):

Draft, Issued, Submitted (Déposée 200), Transmitted (Émise 201), Delivered (Reçue 202), Available (Mise à disposition 203), TakenInCharge (Prise en charge 204), Accepted (Approuvée 205), PartiallyApproved (Approuvée partiellement 206), Disputed (En litige 207), Suspended (Suspendue 208), Completed (Complétée 209), Refused (Refusée 210), PaymentTransmitted (Paiement transmis 211), Settled (Encaissée 212), Rejected (Rejetée 213), Cancelled (Annulée).

stateDiagram-v2
    [*] --> Draft: create (classical model, editable)
    Draft --> Issued: finalize (gap-free number, lock, generate structured model)
    Draft --> [*]: discard draft (drafts may be deleted; finalized invoices may not)

    Issued --> Submitted: plateforme_agreee transmits (200 Déposée)
    Submitted --> Rejected: platform format/technical rejection (213 Rejetée)
    Rejected --> [*]: terminal for this invoice; correct → new invoice + new transmission

    Submitted --> Transmitted: issuer PA hands to recipient PA (201 Émise)
    Transmitted --> Delivered: received by recipient PA (202 Reçue)
    Delivered --> Available: made available to buyer (203 Mise à disposition)
    Available --> TakenInCharge: buyer acknowledges, processing starts (204 Prise en charge)

    TakenInCharge --> Accepted: buyer accepts (205 Approuvée)
    TakenInCharge --> PartiallyApproved: buyer partially accepts (206 Approuvée partiellement)
    TakenInCharge --> Refused: buyer refuses on business grounds (210 Refusée)
    TakenInCharge --> Disputed: dispute raised (207 En litige)

    Accepted --> Disputed: dispute raised after acceptance (207)
    Disputed --> Accepted: dispute resolved in favour (205)
    Disputed --> Refused: dispute resolved against (210 Refusée)

    TakenInCharge --> Suspended: processing suspended (208 Suspendue)
    Suspended --> Completed: supplementary elements provided (209 Complétée)
    Completed --> TakenInCharge: resume

    Accepted --> PaymentTransmitted: payment initiated/ordered (211 Paiement transmis)
    PartiallyApproved --> PaymentTransmitted: 211
    PaymentTransmitted --> Settled: payment confirmed (212 Encaissée)
    Accepted --> Settled: paid in full (212 Encaissée)

    Issued --> Cancelled: cancellation (rare error case, Annulée)
    Submitted --> Cancelled: cancellation (rare error case, Annulée)

    Refused --> [*]: terminal
    Settled --> [*]: terminal
    Cancelled --> [*]: terminal

    note right of Cancelled
        Annulée / Cancelled is for rare error
        cases only. It is NOT a substitute for
        a credit note (avoir, type 381).
        Corrections of a finalized invoice
        issue an avoir — see §5.
    end note

Key characteristics:

3.1 Domain status → business_api projection → reflected AFNOR legal status

This table maps the canonical internal status to (a) the coarse business_api status and (b) the AFNOR legal status the invoice reflects. The AFNOR ↔ B2Brouter API ↔ internal mapping is authoritative in PA 5 §5.1 and PA 5 §10; this table must not contradict it.

Canonical internal status (invoicing)business_api projectionReflected AFNOR legal status (code)
DraftDraft(none — pre-legal)
IssuedPending(none — created in PA, pre-send)
SubmittedPendingDéposée (200)
TransmittedPendingÉmise par la plateforme (201)
DeliveredPendingReçue par la plateforme (202)
AvailablePendingMise à disposition (203)
TakenInChargePendingPrise en charge (204)
AcceptedPendingApprouvée (205)
PartiallyApprovedPendingApprouvée partiellement (206)
DisputedPendingEn litige (207)
SuspendedPendingSuspendue (208)
CompletedPendingComplétée (209)
PaymentTransmittedPendingPaiement transmis (211)
SettledSettledEncaissée (212)
RefusedCanceledRefusée (210)
RejectedCanceledRejetée (213)
CancelledCanceledAnnulée (rare, outside the 14 codes)

Design rule: The business_api set (Draft | Pending | Settled | Canceled) is a presentation-layer projection and is lossy on purpose — it cannot distinguish platform rejection from buyer refusal, nor delivery from acceptance. The domain MUST persist the full canonical set so the mandatory AFNOR statuses remain derivable (PA 5 §9 invariant).

Invariant: The reflected AFNOR status (legal_status) is projected, never authored, by invoicing. The authoritative legal status lives on the plateforme_agreee transmission (referenced by transmission_id); invoicing mirrors it from TransmissionStatusChanged. See §6.

4. Finalization and Immutability

Finalization is the hinge of the aggregate: it turns an editable draft into the immutable AR record.

Finalization, in order:

  1. Validate the classical model against PA 4 §4 — every mandatory EN 16931 term must be populatable (buyer SIREN, delivery address when ≠ billing, VAT category per line, VAT-chargeability option, structured payment means, …). Finalize fails if any mandatory term is missing. Validation also enforces the VAT-regime consistency invariant: finalize is rejected if any line with goods_or_service == goods carries vat_chargeability == encaissement (goods are always on débits; the chargeable event is issuance under CGI art. 269, never collection) — see the no-goods-on-encaissement invariant in 10. Tax and VAT §5/§8. It further enforces the channel classification result (2. Dual Invoice Model §3.5): a buyer SIREN (9-digit, Luhn-valid) is required for the domestic-FR-B2B channel, and a draft classified as B2G or Monaco is rejected/parked (out of MVP scope).
  2. Assign the gap-free number (BT-1) from the issuer’s sequence. Numbering is sequential and gap-free per 5. Numbering.
  3. Generate the EN 16931 structured model from the classical model (2. Dual Invoice Model §6). This is the transmission source of truth.
  4. Lock the classical and structured models — the invoice becomes immutable.
  5. Emit InvoiceFinalized on the eventbus (invoice_id, organisation_id, structured-invoice reference) so plateforme_agreee begins the outbound contract (PA 10 §3).

Invariant: A finalized invoice is immutable. Its fields, number, and structured model are locked. It is never edited and never deleted. A gap-free numbering sequence with a deleted or renumbered entry is non-compliant and breaks the audit trail.

Design rule — issued invoices store the exact transmitted legal artefact; regeneration is display-only. The structured invoice data (the locked classical + EN 16931 models) lives in the database, but the legal archive is the exact final artefact that was transmitted — the PA-generated Factur-X / UBL / CII fetched from B2Brouter via download_legal_url, content-hashed and stored permanently. A later regeneration is not the same legal artefact (renderers, validation rules, attachments, and legal output can change over time), so the transmitted file is fetched and stored, never reconstructed for compliance. On-the-fly regeneration from the immutable DB data is kept only as a display convenience (preview before finalize, re-render for a quick view), never as the compliance archive. See PA 8. Archiving §5.2.

Design rule — the piste d’audit fiable rests on the stored artefact (decided). Authenticity, integrity, and legibility are met by the PAF maintained over the stored legal artefact (its content hash, status history, CDAR events, transaction match, and the immutable structured data), not by regeneration and not by a QES. The PII-bearing transmitted artefact and its supporting evidence follow the four-basis retention model, not “kept forever”: the statutory 6-yr VAT / 10-yr accounting window, then Green-Got’s product archive promise (a distinct lawful basis with offboarding/deletion controls), with permanent retention limited to the non-PII integrity evidence (content hash, PAF chain); any PII outliving the statutory/product window requires legal review. Storage ownership and retention governance are detailed in PA 8. Archiving §2 and PA 13. Privacy §4.

Invariant: A finalized, gap-free invoice cannot be deleted. Only a Draft may be discarded. Any correction to a finalized invoice is a separate avoir (§5), never an in-place edit or deletion. This is a hard requirement of gap-free numbering and the piste d’audit fiable, not a soft convention (PA 4 §6 design rule).

5. Credit Notes (Avoir / Document Type 381)

A finalized invoice cannot be modified or deleted, so any correction — a wrong amount, a returned good, a cancelled order after issuance — is made by issuing an avoir (credit note), document type 381 (PA 4 §6). The avoir is a full first-class AR document, not an annotation.

5.1 The Avoir aggregate

FieldTypeNotes
id“avo_…”Prefixed (avo_), time-sortable.
numberstringIts own gap-free, year-prefixed number (e.g. AV-2026-0001), from a sequence distinct from invoices. See 5. Numbering.
document_typeUNCL1001 = 381Avoir / note de crédit.
corrects_invoice_id“inv_…”The finalized invoice it references. The original invoice is never mutated — an avoir is never an edit of the original.
client_id“cli_…”Same buyer as the corrected invoice.
line_items[]per lineThe amounts being credited (full reversal or partial adjustment), with their VAT category + rate so the credited VAT is explicit.
totalsnet / VAT breakdown / VAT / grossSame EN 16931 totals structure; represents the credited (negative) amount, including the credited VAT per 10. Tax and VAT.
settlement_moderefund | credit_appliedHow the credit is settled: refunded (transfer/cheque to the buyer) or applied as a credit against a future invoice. See §5.3.
statusavoir internal statusSee §5.2.
legal_statusAFNOR, nullableReflected AFNOR legal status, projected from its own transmission via TransmissionStatusChanged.
transmission_idFK to PaTransmission, nullableIts own outbound transmission.
issued_at, created_attimestamps

Design rule: An avoir references the invoice it corrects (corrects_invoice_id); it never edits it. The corrected invoice keeps its number, its structured model, and its place in the gap-free sequence unchanged. The pair (invoice 380 + avoir 381) is the audit-faithful record of the correction.

Design rule — mandatory avoir mentions. As a French facture d’avoir, the avoir must carry the word “avoir”, the reference to the original invoice number (the corrects_invoice_id invoice’s number), the credited HT amount, and the corresponding VAT amount — so the seller may recover (impute or be refunded) the VAT on the credited portion. When VAT is not recovered (or the buyer is established outside France), the avoir is issued “net de taxe” instead. These are first-class structured fields (the credited line items + VAT breakdown), not free text.

Invariant: An avoir may reference only a finalized invoice. A draft is corrected by editing it, not by an avoir.

5.2 Avoir lifecycle

The avoir has its own lifecycle that mirrors the invoice’s: it is authored as a draft, finalized (own gap-free 381 number, own structured model, InvoiceFinalized-equivalent transmission), transmitted by plateforme_agreee, and reflects its own AFNOR legal status. Like an invoice, a finalized avoir is immutable.

stateDiagram-v2
    [*] --> Draft: create avoir referencing a finalized invoice
    Draft --> Issued: finalize (own gap-free 381 number, generate structured model, lock)
    Draft --> [*]: discard draft
    Issued --> Submitted: plateforme_agreee transmits (Déposée)
    Submitted --> Rejected: platform rejection (Rejetée)
    Submitted --> Delivered: delivered (Reçue)
    Delivered --> Accepted: buyer accepts (Acceptée)
    Delivered --> Refused: buyer refuses (Refusée)
    Accepted --> Settled: credit applied / reconciled (Encaissée-equivalent)
    Rejected --> [*]: terminal; correct → new avoir
    Refused --> [*]: terminal
    Settled --> [*]: terminal

Design rule: An avoir is itself an e-invoice (type 381) transmitted over Flux 1 exactly like an invoice. It carries the same EN 16931 mandatory mentions, derives a structured model the same way (2. Dual Invoice Model), and projects its AFNOR legal status the same way (§6). It is not a soft reversal flag on the original invoice.

Invariant: An avoir consumes its own number from its own gap-free, year-prefixed sequence (AV-YYYY-NNNN), distinct from the invoice (380) and quote sequences, and is itself gap-free and immutable once finalized. Numbering mechanics (year-prefix, annual reset, gap-free assignment at finalize) are defined canonically in 5. Numbering.

5.3 Credit semantics

An avoir reduces what the buyer owes. The credited amount (net + VAT) is settled in one of two ways, recorded as settlement_mode:

Design rule — relation to Encaissée. An avoir is a distinct e-invoice (type 381) with its own lifecycle and its own Settled/Encaissée-equivalent terminal state; it does not change the corrected invoice’s status. The corrected invoice keeps whatever legal status it reached (commonly Settled/Encaissée). When the avoir is refunded, its own reconciliation (the outbound refund transfer matched per 9. Transaction Matching) drives the avoir to its Encaissée-equivalent terminal state. When the credit is applied to a future invoice, the avoir reaches its terminal state once the credit is consumed via the credit-allocation construct (§5.5); no separate cash movement settles it. There is no partial-settlement state on the avoir itself — settlement of an avoir, like an invoice, is all-or-nothing (8. Payment Link and Collection).

5.5 Credit-allocation construct (non-bank)

A settlement_mode == credit_applied avoir reduces what a future invoice’s buyer must pay, without moving money. This is not the bank-money payment-allocation / VAT-collection ledger (PA 10 §11) — that ledger only ever records real bank movements, and a credit imputation moves no cash. It is a separate, non-bank CreditAllocation construct owned by invoicing.

FieldTypeNotes
id“cal_…”Prefixed, time-sortable.
avoir_id“avo_…”The credit_applied avoir whose balance is being consumed.
applied_to_invoice_id“inv_…”The future invoice the credit is imputed onto.
applied_amountinteger cents (i64)The TTC amount of avoir credit imputed onto this invoice (with its VAT split, mirroring the avoir’s breakdown).
applied_attimestampWhen the allocation was recorded.

Design rule — a credit-applied avoir carries a consumable balance. A credit_applied avoir has a credit_balance = (avoir TTC) − Σ(applied_amount of its CreditAllocation rows). The balance starts at the full avoir total and is drawn down by allocations; the avoir reaches its Encaissée-equivalent terminal state when credit_balance == 0. The construct is append-only like the bank ledger: a wrong allocation is reversed by appending a negative applied_amount row, never edited.

Design rule — auto vs explicit application. Imputation is explicit by default: the operator chooses which future invoice a credit is applied to, because imputation has VAT-declaration consequences (below) and must be auditable. An optional auto-apply policy (oldest-open-invoice-first for the same client_id) may be enabled per organisation, but the chosen target and order are always recorded on the CreditAllocation row.

Design rule — partial and multi-credit consumption order. A single avoir’s credit may be split across several future invoices (multiple CreditAllocation rows summing to ≤ the avoir total), and a single invoice may consume credit from several avoirs. When multiple credits apply to one invoice, they are consumed oldest-avoir-first (by avoir issued_at), each drawn down to zero before the next, until the invoice’s payable is covered or the credits are exhausted. No allocation may push an avoir’s cumulative applied amount above its total (no over-allocation), mirroring the bank ledger’s invariant.

Design rule — encaissement-VAT régularisation timing (BOFiP). The VAT on a credited (encaissement-regime) amount is regularised by the seller via the avoir, and the mechanism differs by settlement_mode:

The two paths are not interchangeable for the VAT timing, which is why settlement_mode is a first-class field. See §8 Sources (BOFiP).

Invariant — the credit-allocation construct moves no money. A CreditAllocation never appears in the bank payment-allocation ledger and never emits PaymentCollected for a cash movement. The future invoice’s settlement target is net of applied credit (see 9. Transaction Matching §5): the bank transaction need only cover (invoice TTC − Σ applied credit) for the invoice to settle in full.

5.4 Implementation-plan note

6. Reflected Legal Status

The AR invoice (and avoir) only projects the legal lifecycle status; it never owns it. The authoritative AFNOR status lives on the plateforme_agreee transmission.

sequenceDiagram
    autonumber
    participant INV as invoicing (AR)
    participant PA as plateforme_agreee
    participant B2B as B2Brouter (PA)

    INV->>INV: Finalize invoice (gap-free number, lock, structured model)
    INV-->>PA: emit InvoiceFinalized { invoice_id, org_id, structured ref }
    PA->>B2B: create + send transmission
    B2B-->>PA: webhook / CDAR (Déposée / Reçue / Acceptée / Refusée / Rejetée …)
    PA->>PA: map to authoritative AFNOR legal status on transmission
    PA-->>INV: emit TransmissionStatusChanged { transmission_id, invoice_id, legal_status, changed_at, reason? }
    INV->>INV: update legal_status + advance canonical internal status
    Note over INV: Settled / Encaissée is driven by Green-Got collection, not by B2Brouter

Design rule: invoicing updates legal_status (and transmission_id) and advances the canonical internal status only on receipt of TransmissionStatusChanged. It never computes or writes the AFNOR status itself. The exception is the payment-driven path: Encaissée / Settled originates from Green-Got’s own collection. Transaction matching emits TransactionMatched (the commercial settlement event), which triggers the explicit MarkOutboundInvoicePaid command; that command — distinct from PaymentCollected — yields AFNOR status 212 (Encaissée), sent to the recipient’s PA when legally applicable (CGI art. 290 A / VAT-on-collection) and reflected back via TransmissionStatusChanged. PaymentCollected is a separate projection: the bank-level VAT-collection ledger entry and the Flux 10 payment-data source. The same bank collection can cause both, but they are different legal projections. See PA 10 §8 and 9. Transaction Matching.

Invariant: The reflected legal status is a read-model projection. It is never the regulatory record and must never be treated as authoritative for compliance; the authoritative AFNOR status is reconstructable only from the plateforme_agreee transmission’s persisted B2Brouter API status + CDAR (PA 5 §10).

7. Related Documents

8. Sources

4. Quote Domain

Quote Domain

This document is the authoritative target-design specification for the Quote (devis) aggregate in the invoicing crate — its fields, lifecycle state machine, numbering, signature handoff, conversion to an invoice, reminders, and archival — and the invariants that govern it.

Terminology

1. Overview

A quote is the pre-sales document of the AR (accounts-receivable) flow. The organisation drafts a priced offer, sends it to a client, optionally chases it with reminders, obtains an e-signature, and — once signed — converts it into a draft invoice that then enters the regulated e-invoicing lifecycle.

Design rule: A quote uses Green-Got’s own internal model. Unlike an issued invoice, a quote has no legal e-invoice format obligation — it is not transmitted through a Plateforme Agréée, not structured as Factur-X / UBL / CII, and does not carry AFNOR lifecycle statuses. The regulated lifecycle begins only after conversion, on the resulting invoice. See 3. Invoice Domain and ../../plateforme_agreee/docs/5_lifecycle_statuses.md.

Design rule: The e-signature itself is out of scope for this domain. Invoicing presents the quote, sends it for signature, and reacts to the outcome. The signature flow, request/callback contract, signature levels (SES up to a HIGH face-recognition level), and audit-trail retention live in 6. Quote Signature; the signature is performed by the external Green-Got service ggbs.

The quote aggregate owns:

2. Quote Entity

The Quote aggregate is identified by a quote_id (prefixed time_sortable_id, quo_). It reflects the live business_api Quote shape (number, client, line items, validity/expiry, status), enriched with the fields the target design requires.

2.1 Fields

FieldTypeNotes
idQuoteId (quo_…)Primary key; time_sortable_id.
organisation_idOrganisationIdOwning organisation.
client_idClientId (cli_…)Recipient client.
numberOption<QuoteNumber>e.g. DEV-2026-0012. None while Draft; assigned at send. See §4 and 5. Numbering.
statusQuoteStatusCanonical domain status; see §3.
currencyISO 4217Document currency.
issue_dateOption<Date>Set when sent.
validity_daysi16Validity duration; default 30 days.
expires_atOption<Date>Resolved expiry = issue_date + validity_days.
line_itemsVec<LineItem>Description, quantity, unit price (cents, i64), VAT rate.
total_amountcents (i64)Computed gross (TTC) from line items; denormalised for listing.
noteOption<String>Free-text note shown on the quote.
signature_idOption<SignatureId>Reference to the in-flight / completed signature record (see 6. Quote Signature).
converted_invoice_idOption<InvoiceId>Set on conversion; prevents double-conversion.
pdf_file_idOption<String>Reference to a retained rendered quote PDF in the core S3 bucket. Null while the quote is regenerated on demand (unsigned renders are generated on the fly, not stored); set once a signed artefact must be retained — at signature the specific rendered PDF the client signed is stored and referenced here (see §8 and 6. Quote Signature).
client_nameStringDenormalised client name for listings.
created_attimestamptz
updated_attimestamptz
sent_atOption<timestamptz>First send.
viewed_atOption<timestamptz>First time the client opened the quote / signing link. Informational only — it does not advance, reset, or suppress the reminder cadence; view-based suppression is deferred (7. Reminders §8).
signed_atOption<timestamptz>Set from the signature outcome.

Validation rules:

2.2 business_api projection

The live business_api Quote exposes a coarse status set Draft | Pending | Signed | Cancelled and the fields number, client_id, client_name, status, total_amount, currency, issued_at, expires_at, created_at. This is a presentation projection of the richer canonical domain status set defined in §3.

business_api statusCanonical domain status(es) it projects
DraftDraft
PendingSent, Viewed
SignedSigned
CancelledDeclined, Expired, Cancelled

Design rule: The business_api set is lossy on purpose and MUST NOT be the persisted state. The domain persists the full canonical status so that “viewed but not signed”, “declined”, and “expired” remain distinguishable — they drive reminders, re-send, and reporting differently.

3. Quote State Machine

The canonical domain status set is Draft, Sent, Viewed, Signed, Declined, Expired, Cancelled. The signature-outcome transitions (Signed, Declined, Expired) are driven by the external ggbs service via the contract in 6. Quote Signature.

stateDiagram-v2
    [*] --> Draft: create

    Draft --> Sent: send for signature (assign number, render PDF, submit to ggbs)
    Draft --> [*]: delete (Draft only — no number assigned)

    Sent --> Viewed: client opens quote / signing link
    Sent --> Signed: signature outcome = signed
    Sent --> Declined: signature outcome = declined
    Sent --> Expired: validity / signature expires
    Sent --> Cancelled: organisation cancels
    Sent --> Sent: convert to Draft Invoice (action; lifecycle state unchanged)

    Viewed --> Signed: signature outcome = signed
    Viewed --> Declined: signature outcome = declined
    Viewed --> Expired: validity / signature expires
    Viewed --> Cancelled: organisation cancels
    Viewed --> Viewed: convert to Draft Invoice (action; lifecycle state unchanged)

    Signed --> Signed: convert to Draft Invoice (action; quote stays Signed)

    Declined --> Sent: re-send (new signature request)
    Expired --> Sent: re-issue as a new quote (fresh validity window, new signature request)

    Declined --> Cancelled: archive
    Expired --> Cancelled: archive
    Signed --> Cancelled: archive (post-conversion / abandoned)

    Cancelled --> [*]: terminal (archived)

    note right of Sent
        Number is assigned at send (French devis are numbered).
        Reminders may be scheduled here (doc 7).
    end note

    note right of Signed
        Conversion is an ACTION (sets converted_invoice_id), not a
        lifecycle transition — it does not move the quote to a new
        status. Signed is display-terminal but NOT absorbing: a
        converted quote keeps its Signed status until archived to
        Cancelled. Cancelled is the only true absorbing terminal.
        Conversion is NOT restricted to Signed: a quote may be
        accepted outside our signature flow and converted from any
        live sent state (Sent, Viewed, or Signed). Conversion is
        one-shot. Once signed, the quote content is immutable.
    end note

    note right of Expired
        Expiry is TERMINAL for signing and for conversion: an Expired
        quote is non-signable and non-convertible. The only forward
        action is RE-ISSUE as a new quote (Expired → Sent, fresh
        validity window + new signature request). See §5.1 (AR-3).
    end note

Key characteristics:

Design rule: Route handlers in business_api carry no business logic; the transitions above are enforced by invoicing use_cases, not by the presentation layer.

4. Quote Numbering

French commercial practice and the devis rules require quotes to be numbered with a dedicated, distinct series from invoices. Green-Got uses a quote-specific prefix, by default DEV, producing identifiers such as DEV-2026-0012.

Design rule: A quote’s number is assigned at send, not at creation — a Draft quote has no number, consistent with the deletability of drafts. This mirrors, but is independent from, invoice numbering: quotes and invoices draw from separate series so a quote number never collides with or consumes an invoice number.

Design rule: Although quotes are not fiscal documents, Green-Got allocates quote numbers through the same concurrency-safe, per-organisation, per-series allocator used for invoices, so that the quote series is contiguous and auditable. The allocation invariant, prefix/year/sequence scheme, annual-reset configurability, and concurrency model are defined once in 5. Numbering.

Invariant: A Draft quote has number = None. Once a number is assigned at send, it is immutable for the life of the quote.

5. Conversion of a Quote to a Draft Invoice

A quote is converted into a new Draft invoice, never mutated into one. Conversion is an explicit organisation action available from any live sent quote — Sent, Viewed, or Signednot only from Signed, but never from Expired.

Design rule: Conversion is not gated on our signature flow. A quote may have been accepted outside Green-Got’s e-signature process (e.g. agreed by email or in person), so the organisation may convert it without a Signed outcome. The states that cannot convert are Draft (no number, not yet a real offer), Cancelled (archived), and Expired (the offer has lapsed — it must be re-issued as a new quote first, see §5.1). Conversion is still one-shot regardless of the source state.

Conversion:

  1. Creates a new Invoice in status Draft (no invoice number yet — invoice numbers are assigned only at finalize; see 3. Invoice Domain and 5. Numbering).
  2. Copies the quote’s client_id, currency, and line items into the new invoice.
  3. Records the linkage on both sides: the invoice references the originating quote_id; the quote records converted_invoice_id.

Invariant: A quote converts at most once. converted_invoice_id is set transactionally at conversion; a second conversion attempt is rejected.

Design rule: Conversion copies a snapshot of the line items into the invoice. After conversion the invoice is an independent aggregate and can be edited (while Draft) without affecting the signed quote, which remains immutable. The fiscal/e-invoicing obligations attach to the resulting invoice, not the quote — the quote is and remains an internal-model document.

Design rule — conversion does not change the quote’s status. Conversion is an action that sets converted_invoice_id; it does not transition the quote to a new lifecycle status. A converted Signed quote stays Signed, a converted Viewed quote stays Viewed, and so on. The quote later leaves the workflow only by archival to Cancelled (§8). This is why a Signed quote can subsequently be Cancelled: Signed is display-terminal (no further signature outcome) but not absorbing. (Expired is not convertible at all — it must be re-issued as a new quote first, §5.1.)

5.1 Expired quote — re-issue, not late acceptance

Decision (AR-3). An Expired quote has passed its expires_at, so the offer has lapsed: it can no longer be signed and can no longer be converted. Expiry is terminal for both signing and conversion. There is no direct conversion / late-acceptance path for an expired quote.

The only forward action is re-issue: the system notifies the customer and offers to re-issue a new quote based on the expired one. Re-issue produces a new quote — its own quote_id, its own number assigned at send, and a fresh validity window — which is then sent for signature afresh through ggbs (a new signature request). In the state machine this is modelled as Expired → Sent (re-issue), carrying the new validity window and signature request; the prior signature record is archived.

Why no late acceptance. Converting a lapsed offer would attach fiscal obligations to an invoice derived from a quote the client never validly accepted within its validity window. Forcing a re-issue keeps the accepted offer and the resulting invoice anchored to a live quote with a current validity window and (where used) a current eIDAS signature.

Invariant: An Expired quote is non-signable and non-convertible. Its only forward action is re-issue as a new quote (Expired → Sent); the original expired quote is left as the historical record (and may be archived to Cancelled). A new eIDAS signature, where used, needs the live validity window the re-issued quote provides.

6. Reminders

A sent, unsigned quote may be chased with reminders (e.g. before expiry, after no response) to prompt the client to sign. Quote reminder cadence, channels (email / push), scheduling, and skip conditions (stop once the quote is Signed, Declined, Expired, or Cancelled) are defined in 7. Reminders.

Design rule: Reminders never alter the quote’s status; they are a side channel. Status only changes via the transitions in §3.

7. Signature Handoff

The e-signature is performed by the external Green-Got service ggbs, not by the invoicing crate. Invoicing’s responsibilities at the boundary are:

The full contract — the signature flow, request shape, callback outcomes, signature levels (SES up to a HIGH face-recognition level), permanent audit-trail retention, and the statement that the signature is owned by ggbs (the Green-Got business unit that also provides VOP and FNCRF) — is specified in 6. Quote Signature.

8. Archival

A quote leaves the active workflow by being archived to the Cancelled state, never by hard deletion (except a Draft, which may be deleted because it has no number and no fiscal trace).

Invariant — post-signature immutability: Once a quote is Signed, its content (client, line items, total, number) is immutable. The signed PDF and its audit trail are the evidentiary record of what the client agreed to; corrections are made by issuing and sending a new quote, not by editing the signed one.

Design rule — an unsigned quote render is regenerated on the fly; a signed quote is stored. A signature binds a specific rendered document, so the signed quote PDF + the signature audit trail ARE retained as stored artefacts in the core S3 bucket and cannot be regenerated identically — they are kept (pdf_file_id + the audit-trail reference). An unsigned quote render is generated on the fly when asked and not stored: pdf_file_id is null until a signed artefact must be retained. (Issued invoices are different and not governed by this rule: an invoice’s render is produced on the fly for display, but its transmitted legal artefact is stored as the compliance archive — see 3. Invoice Domain §4.) Retention rules for the signed artefacts are in 6. Quote Signature §6.

Invariant — validity expiry: A quote that passes its expires_at without being signed transitions to Expired and can no longer be signed or converted — the offer has lapsed (AR-3). The only forward action is re-issue as a new quote (§5.1), which assigns a new number, sets a fresh expires_at, and opens a new ggbs signature request (Expired → Sent). There is no direct conversion / late acceptance of an expired quote.

Design rule: Archival affects only the quote’s lifecycle. It does not delete the retained signed PDF, the signature record, or the audit trail; those stored artefacts are kept per the signature retention rules in 6. Quote Signature. (Unsigned quote renders were never stored — they are regenerated on the fly — so there is nothing to delete for them.)

9. Related Documents

10. Sources

Quotes (devis) use Green-Got’s own internal model and carry no e-invoice format obligation — a devis is not a regulated e-invoice. The legal points referenced here:

5. Numbering

Numbering

This document is the authoritative target-design specification for legally-compliant document numbering in the invoicing crate — the French gap-free sequential requirement, the prefix-year-sequence scheme, per-organisation per-series allocation, annual-reset configurability, the concurrency-safe allocation invariant, the coupling between number assignment and finalization, and what happens when a document is voided.

Terminology

1. The French Legal Requirement

French law requires that invoices issued by a legal entity be numbered with a unique, chronological, gap-free sequential number, based on a continuous sequence. Numbering may be organised into one or more distinct series (e.g. per establishment or per document type), but within each series the chronological gap-free property must hold: numbers are allocated in time order and no number may be skipped or reused.

The practical consequences for the domain:

Design rule: The gap-free requirement attaches to the issued/finalized document. Drafts have no number and are exempt — a Draft may be freely edited or deleted because it occupies no slot in any series. See §6.

2. The Numbering Scheme

Every number is {prefix}-{year}-{sequence}:

ComponentDescriptionExample
prefixA short, document-type-specific, per-organisation-configurable string.FAC, DEV, AV
yearThe four-digit year of issuance.2026
sequenceZero-padded sequence number within the series, monotonically increasing.0001, 0012
Document typeDefault prefixExample numberSeries
Issued invoiceFACFAC-2026-0001Invoice series
Quote (devis)DEVDEV-2026-0012Quote series
Credit note (avoir, type 381)AVAV-2026-0003Distinct credit-note series

Design rule: Invoices, quotes, and credit notes draw from separate series. A credit note never consumes an invoice number and vice versa; a quote number never collides with an invoice number. This keeps each fiscal series independently gap-free and chronologically ordered.

Design rule: The default prefixes (FAC, DEV, AV) are configurable per organisation. The prefix is part of the series identity; changing an organisation’s prefix is a configuration concern and must not retroactively renumber already-issued documents.

3. Per-Organisation Series

Design rule: Numbering series are scoped to the organisation (the legal entity). Two organisations issue numbers independently; their series never share a counter. The series key is (organisation_id, document_type[, year]) — the year component participates in the key only when annual reset is enabled (see §4).

This means a single Green-Got deployment serves many organisations, each with its own gap-free invoice series, its own quote series, and its own credit-note series.

4. Annual Reset vs Continuous

Design rule: Numbering resets annually and the number is year-prefixed — this is the decided default for every series (invoices, quotes, credit notes). The sequence restarts at 0001 each calendar year, the year is part of the series key, and the rendered number carries the issuance year: FAC-2025-0149 is followed in 2026 by FAC-2026-0001, DEV-2026-0001, AV-2026-0001. This is the common French practice.

French law also permits a continuous scheme (a single unbroken sequence that never resets; the year shown in the rendered number is the issuance year but does not reset the counter, FAC-2025-0149 → FAC-2026-0150). It remains legal and may be offered as a per-organisation alternative, but annual reset is the default.

Design rule: When annual reset is in effect (the default), year is part of the series key; under the continuous alternative it is not (the counter spans years).

Invariant: Whichever mode is chosen, the series remains gap-free and chronological. Switching modes is a forward-only configuration change and must not renumber already-issued documents or create a gap.

5. Concurrency-Safe Allocation

Two requests finalizing invoices for the same organisation at the same instant must not receive the same number, and must not skip a number. The allocator therefore serialises allocation per series and is atomic with the finalization that consumes the number.

Invariant (gap-free allocation): For a given series, the allocator returns the smallest sequence number not yet issued, exactly once. Concurrent allocations for the same series are serialised so that they receive strictly consecutive numbers; no two callers receive the same number and no number is skipped.

The allocation approach (described as an invariant, not pinned to a specific SQL form):

Design rule: A number is allocated only when it will be consumed. The allocator does not hand out a number speculatively to a caller that might roll back — because a rolled-back allocation that nonetheless advanced the counter would create a gap. Allocation and consumption are one atomic unit.

Invariant (no double allocation under retry): If a finalization transaction is retried, it must not consume two numbers. The allocation is part of the finalization transaction; a retried-and-rolled-back attempt leaves the counter unchanged, and the successful attempt consumes exactly one number.

flowchart TD
    Start["Finalize invoice (or send quote)"] --> Begin["BEGIN transaction"]
    Begin --> Lock["Serialise the series
(advisory lock / atomic upsert on
organisation_id, document_type, year?)"] Lock --> Incr["last_number = last_number + 1
format number (e.g. FAC-2026-0001)"] Incr --> Write["Write number + first issued status
on the document"] Write --> Commit{"Commit succeeds?"} Commit -->|"yes"| Done["Number consumed exactly once;
series gap-free"] Commit -->|"no / retry"| Rollback["ROLLBACK — counter unchanged,
no number consumed, no gap"] Rollback --> Begin Done -.->|"later: reverse/correct"| Avoir["Issue credit note (avoir, 381)
from the distinct credit-note series"] Avoir -.-> NoGap["Original keeps its number;
both series stay gap-free"]

5.5 Crash recovery and gap visibility

The atomicity above protects the common case; this section states what happens when a process crashes mid-finalize and how a gap, if it ever occurs, is made visible rather than silently recovered.

Design rule — pre-commit crash rolls back, leaving no gap. Because the counter increment and the document write are in one transaction (§5), a crash before commit rolls the whole transaction back: the counter is unchanged and no number was consumed. There is no “reserved-then-orphaned” number, because the allocator never hands out a number that could outlive its transaction. A retried finalize then takes the same next number cleanly.

Design rule — high-water-mark model, never decrement. last_number is a monotonic high-water mark: it only ever increases, and a committed number is never released or reused even if the surrounding workflow later fails downstream (e.g. transmission is rejected). A Rejetée re-submission corrects via a new transmission or an avoir, never by reclaiming the number — consistent with §7.

Design rule — gaps are surfaced by a daily reconciliation, not silently healed. A gap should be impossible under the atomic allocator, but a defence-in-depth daily reconciliation scans each active series for any missing sequence value between 1 and last_number. If a gap is found it raises a visible operator alert (it is a compliance defect — the piste d’audit fiable requires a continuous series) and is never silently back-filled or auto-closed: a real gap means a finalized document is missing or a number leaked, which a human must investigate. The reconciliation reads the committed (series, sequence) rows; it does not mutate the counter.

Invariant: A finalize that crashes before commit consumes no number (counter unchanged); a finalize that commits consumes exactly one. Any detected gap is alerted, not auto-repaired.

6. Coupling to Finalization

Number assignment is bound to the transition that makes a document legally issued:

Invariant: Drafts have no number. number = None for any Draft invoice, any Draft quote, and any not-yet-issued credit note. A Draft can be edited or deleted freely precisely because it holds no slot in any series.

Design rule: Number allocation and the issued-status write are atomic. There is no window in which a document is finalized but unnumbered, nor one in which a number is consumed but its document is absent. If either half fails, both roll back, the counter is unchanged, and no gap is created.

7. Void and Credit Notes (No Gaps)

A finalized invoice cannot be deleted or renumbered, because either would create a gap in the gap-free series. To reverse or correct a finalized invoice, the organisation issues a credit note (avoir, document type 381) from the distinct credit-note series.

Design rule: Voiding the economic effect of a finalized invoice is done with a credit note, not by removing the invoice or its number. The original invoice keeps its number and its slot; the credit note takes the next number in the credit-note series and references the invoice it corrects. The two documents together leave both series gap-free.

Invariant (no gap on void): A number, once issued, is never released. There is no operation that deletes a finalized invoice and reclaims its number. The only way to neutralise a finalized invoice is a credit note, which itself is gap-free-numbered.

Avoir numbering. A credit note draws from its own numbering series — gap-free, chronological, year-prefixed (default prefix AV, e.g. AV-2026-0001), keyed by (organisation_id, credit_note, year) and subject to the same annual-reset default and concurrency-safe allocation as the invoice and quote series (§4, §5). Its number is assigned at issuance of the avoir, atomically, exactly like an invoice number is assigned at finalize. The credit-note model (when an avoir is required, corrects_invoice_id linkage to the corrected invoice, refund vs credit-applied semantics, avo_ id prefix) is specified in 3. Invoice Domain — this document owns only the avoir’s numbering.

8. Rendered-Number Constraints and the Internal Canonical Id

The rendered number ({prefix}-{year}-{sequence}) is the legal, human-facing invoice identifier — EN 16931 BT-1. It is constrained by the reform’s invoice-identifier rules and must be validated before finalization, because once a number is consumed it is permanent (§1). Separately, Green-Got keeps an internal canonical id that is never the legal number.

8.1 The French invoice-identifier rules (validated before finalize)

The AFNOR / DGFiP external specifications constrain the rendered invoice identifier (BT-1). The relevant rules, verified against AFNOR XP Z12-012 (the Spécifications externes B2B, business rules BR-FR-*):

RuleConstraintApplies to
BR-FR-01The invoice identifier must be at most 35 characters.BT-1 (and BT-25)
BR-FR-02Alphanumeric only (A-Z, a-z, 0-9) plus -, +, _, /. Must not be only spaces, must not start/end with a space, and must contain no consecutive spaces.BT-1 (and BT-25)
BR-FR-CO-02The invoice uniqueness key is the triple (BT-1 number, issue year from BT-2, seller SIREN BT-30). A second invoice matching all three is rejected by the platforms; the uniqueness control is systematically blocking. Under a billing mandate the number must carry a root proper to the mandataire to avoid collisions with the mandant’s.BT-1, BT-2, BT-30

Note on the “20-character” claim. Some reform commentary cites a 20-character handling for invoice identifiers in certain fluxes. The authoritative AFNOR XP Z12-012 rule is 35 characters (BR-FR-01); no 20-character limit on BT-1 was found in the external specifications. Green-Got therefore enforces 35 as the maximum rendered-identifier length and treats any narrower figure as unverified. (If a specific downstream flux is later confirmed to truncate at 20, that would be an additional, flux-scoped constraint layered on top of the 35-character BT-1 rule — not a replacement for it.)

Design rule — set the maximum before finalization. Green-Got computes the maximum possible rendered length of a series from its configuration (prefix length + separators + year + zero-padded sequence ceiling) and guarantees it stays within 35 characters for every number the series can ever emit, including the highest sequence reachable in a year. The check is a finalize-time and config-time invariant, not a per-document afterthought.

Design rule — reject non-compliant or collision-prone configurations. A numbering configuration is rejected at configuration time if it can generate a number that:

A config that fails any of these is not saved; the operator must amend the prefix, padding, or series split before any document is finalized against it.

8.2 Internal canonical id vs the display/legal number

Green-Got stores two distinct identifiers for every invoice, and they are never the same value:

IdentifierWhat it isWhen it existsMutable?Used for
Internal canonical id (inv_…, a prefixed time_sortable_id)An opaque, globally-unique, collision-proof internal key.At draft creation — before any number is assigned.Never.Primary key, foreign keys, event payloads, URLs, idempotency.
Display / legal number (BT-1)The gap-free rendered {prefix}-{year}-{sequence}.Only at finalize (invoices) / send (quotes) / issuance (avoir).Never, once assigned.The legal invoice identifier shown on the document and transmitted.

Design rule — the canonical id is distinct from the legal number. The internal canonical id is not derived from the rendered number and carries no fiscal meaning: it exists on Drafts (which have no number at all — §6), it is collision-proof by construction (time_sortable_id, not a per-series counter), and it never changes even though the legal number is assigned later. Conversely the legal number is the only identifier that must satisfy the AFNOR gap-free and BR-FR rules. Systems key on the canonical id; humans and the tax administration read the legal number.

Invariant: Every invoice has a stable internal canonical id from creation; the legal BT-1 number is assigned only at finalize and is validated against §8.1. The two are never conflated: a missing or not-yet-assigned legal number never blocks internal references, and the canonical id is never transmitted as BT-1.

8.3 What B2Brouter sends vs what Green-Got stores

Number assignment is Green-Got’s responsibility, not B2Brouter’s. Green-Got generates the gap-free, BR-FR-compliant BT-1 and hands it to plateforme_agreee, which submits it to B2Brouter; B2Brouter transmits and tracks it but does not mint it.

IdentifierMinted byStored by Green-GotSent to / returned by B2Brouter
Internal canonical id (inv_…)Green-Got (draft creation)Yes — primary key.Not sent as a legal field; may travel as an opaque external reference for correlation only.
Legal number (BT-1)Green-Got (finalize, gap-free)Yes — the permanent legal number.Sent as BT-1; transmitted verbatim. B2Brouter does not renumber it.
B2Brouter transmission idB2BrouterYes — stored alongside the invoice for status correlation (3. Invoice Domain).Returned by B2Brouter on submission; it is not the legal number and is never rendered on the document.

Design rule: The legal number is Green-Got’s source of truth and is generated before transmission so it can be validated against the BR-FR rules locally; B2Brouter’s XSD/Schematron validation is a second line of defence, exactly as for the structured invoice (2. Dual Invoice Model §4). The B2Brouter transmission id is a correlation handle Green-Got persists, never a substitute for BT-1.

9. Invariants Summary

Invariant: Within each (organisation, document_type[, year]) series, numbers are gap-free, chronological, and unique.

Invariant: A number, once assigned to a finalized document, is immutable and never reused.

Invariant: Drafts carry no number; numbers are assigned only at finalize (invoices), send (quotes), or issuance (credit notes), atomically with that transition.

Invariant: Allocation is serialised per series and consumed in the same transaction as finalization, so the counter advances only when a document is committed — never speculatively, never on rollback.

Design rule: Quotes, invoices, and credit notes use separate series with separate prefixes; no cross-series collision or sharing of counters.

10. Related Documents

11. Sources

6. Quote Signature

Quote Signature

This document is the authoritative target-design specification for the e-signature flow and contract the invoicing crate depends on to get quotes signed — the signature flow (quote UI → send for signature → client email/link → consult & sign), the request/callback interface, the signature levels, what invoicing persists, audit-trail retention, and how the outcome drives the quote state machine. The signature is performed by the external Green-Got service ggbs; this document defines the flow and interface, not the ggbs implementation.

Terminology

1. Scope

Design rule: The e-signature is performed by the external Green-Got service ggbs, not by invoicing. Invoicing is a consumer of the ggbs signature interface. This document defines the flow and that interface — the request invoicing sends and the outcomes it receives — and nothing about how ggbs technically produces the signature.

In scope here:

Out of scope here (owned by ggbs):

In scope (receiver-side): the callback-security controls invoicing applies when receiving a ggbs callback — constant-time signature verification, timestamp freshness, a processed-event store, raw-body hashing, and the signing-URL lifecycle — are specified here (§4.3). What ggbs does internally is out of scope; how invoicing safely consumes the callback is not.

Design rule: ggbs specifics — its endpoints, the signing-scheme internals, and the face-recognition implementation — are owned by ggbs. Invoicing must not encode ggbs-internal details; it talks to the ggbs abstraction. But invoicing owns the receiver-side hardening of the public callback (§4.3), exactly as the PA integration owns B2Brouter webhook verification — receiving a public callback without replay/freshness protection is not delegated to ggbs. (ggbs is the same Green-Got service that already provides VOP and FNCRF.)

2. Signature Flow

The signature is driven from the quote UI and carried out by ggbs:

  1. From the quote UI, the customer sends the quote for signature (this is the Sent transition in 4. Quote Domain §3: the number is assigned and the PDF rendered, then the signature request is submitted to ggbs).
  2. ggbs emails the client a link.
  3. The link opens a tab where the client can consult and sign the quote (at the requested level — see §3).
  4. ggbs returns the outcome asynchronously, which drives the quote state machine (see §4).

3. Signature Levels

ggbs offers a range of signature levels, from a simple level up to a HIGH level:

Design rule: Invoicing requests a level from ggbs (default SES, optionally the HIGH face-recognition level); ggbs performs the chosen level and returns the outcome. The level requested is recorded with the signature.

Reference: eIDAS Regulation (EU) 910/2014, Article 25 — see §8.

4. Request / Callback Contract

The contract is asynchronous: invoicing submits a signature request to ggbs and later receives an outcome. The two halves:

4.1 Request (invoicing → ggbs)

When a quote is sent for signature (see §2 and 4. Quote Domain §3), invoicing submits a signature request carrying:

FieldDescription
quote_idThe quote being signed; used to correlate the eventual outcome.
documentThe rendered quote PDF (or a reference to it in the file store).
signerThe client signer’s identity (name, email).
signature levelSES by default, or the HIGH face-recognition level (see §3).
expiryThe signing deadline, aligned with the quote’s validity (expires_at).

ggbs returns, synchronously, a signature reference (the opaque request/instance id) and emails the signer a link (which opens a tab to consult and sign). Invoicing persists the reference on the quote (signature_id).

4.2 Callback (ggbs → invoicing)

ggbs later delivers an outcome for a given signature reference. Invoicing accepts exactly these outcomes and maps each to a quote transition:

OutcomeCarriesQuote transition
signedsigner reference, signed_at, audit-trail referenceSent/ViewedSigned
declinedsigner reference, reason?Sent/ViewedDeclined
expiredSent/ViewedExpired
viewed (informational)SentViewed (signer opened the link)

Design rule: Outcomes are idempotent to apply. ggbs may redeliver a callback; invoicing must treat a repeated outcome as a no-op once the quote has reached the corresponding state. The ggbs-internal signing scheme is ggbs’s, but the receiver-side verification, replay/freshness protection, and dedupe of the raw callback are invoicing’s responsibility and are specified in §4.3.

4.3 Callback security and signing-URL lifecycle

The ggbs callback is a public, internet-reachable endpoint that mutates quote state, so invoicing hardens it with the same controls the PA integration applies to B2Brouter webhooks (PA — b2brouter/6. Statuses §4.2):

Design rule — the signing URL is never durable. The signer’s signing URL is a bearer link (whoever holds it can open the signing session and view signer documents). It is persisted only while the request is in flight and is cleared on the first terminal outcome (signed / declined / expired) and on TTL expiry, whichever comes first — it is never retained after completion and never part of the permanent record. The signing URL is a category-4 “never durable beyond its short in-flight TTL” value; only the opaque signature_id reference is kept.

Design rule: Invoicing never authors Signed / Declined / Expired directly. These states are produced only by applying a signature outcome (or, for expiry, by the validity timer) — see 4. Quote Domain §3.

5. What Invoicing Persists

For each quote signature, invoicing persists — against the quote — the minimum needed to correlate, drive state, and retain evidence:

Persisted fieldPurpose
signature reference (signature_id)Correlates callbacks to the quote; opaque handle into ggbs.
signature levelThe level requested / performed (SES or HIGH face-recognition).
audit-trail file referenceReference to the stored audit-trail document (the evidentiary record).
signed_atTimestamp of completed signature, taken from the outcome.
signing URL?The signer’s bearer link, held only while in flight — short TTL, and cleared on the first terminal outcome (signed/declined/expired) or TTL expiry; never durable after completion (§4.3).
statusThe signature’s own status mirror (pending / signed / declined / expired).

Design rule: Invoicing stores references, not the raw signed document bytes or ggbs payloads, on the quote. The signed PDF and audit-trail document are retained as stored artefacts in the core S3 bucket; invoicing keeps the references (pdf_file_id for the signed PDF, the audit-trail file reference). This mirrors the eventbus design rule that records carry ids/refs, not bytes.

Design rule — the signed quote is the storage exception (and issued invoices are stored as legal artefacts). A signature binds a specific rendered document, so the signed PDF cannot be regenerated identically and must be retained. This is the deliberate exception to the on-the-fly policy for quotes: an unsigned quote render is a display copy generated on demand from immutable DB data and is not stored, whereas the signed quote PDF + its audit trail are kept (see 4. Quote Domain §8). Issued invoices are not an exception to retention: the exact transmitted legal artefact of an issued invoice is stored as the legal archive (PA — 8. Archiving §5.2); only an unsigned display copy / on-screen render of an invoice is regenerated on the fly from immutable DB data (3. Invoice Domain §4). On-the-fly rendering is a display convenience, never a substitute for the stored legal artefact.

6. Audit-Trail Retention

Invariant — the signed-quote audit trail is PII-bearing and follows the four-basis retention model, not “forever”. The signed PDF and its audit trail (signer identity, IP, consent, and — at the HIGH level — face-recognition-backed evidence) are personal data, so they are retained under the four bases settled in PA — 13. Privacy §2, §4 and PA — 8. Archiving §2:

  1. Statutory legal-obligation window — kept for the 6-yr VAT / 10-yr accounting period (the evidence-of-agreement minimum).
  2. Product archive promise — availability beyond the statutory window rests on Green-Got’s product archive promise, a distinct lawful basis with its own offboarding/deletion controls and customer self-service access — not an indefinite legal obligation.
  3. Permanent only for non-PII integrity evidence — only the document content hash (and other non-PII integrity evidence) is kept permanently outright.
  4. Legal review — if any PII-bearing signed-quote evidence must truly outlive the statutory/product window, that requires explicit legal review; it is never the default.

Design rule: Retention of the audit-trail document is a storage responsibility — the signed PDF and audit trail are stored as artefacts in the core S3 bucket under the bases above; invoicing only holds the references, and the artefact is subject to the offboarding/erasure controls of the product archive promise once the statutory window elapses. Whether ggbs also archives the trail is a property of ggbs and does not relieve Green-Got of holding its own retained reference.

Reference for the 10-year accounting retention minimum: French e-invoicing reform archiving rules — see §8 and ../../plateforme_agreee/docs/8_archiving_and_audit.md.

7. Signature Handoff Sequence

The flow-level handoff (ggbs-internal details deliberately omitted — they are owned by ggbs):

sequenceDiagram
    autonumber
    participant ORG as Organisation user
    participant INV as invoicing (AR)
    participant GGBS as ggbs (external GG service)
    participant CLIENT as Client signer
    participant FILES as File store (retained per four-basis model)

    ORG->>INV: send quote for signature (quote UI)
    INV->>INV: assign number, render quote PDF (doc 4, doc 5)
    INV->>GGBS: submit signature request { quote_id, PDF, signer, level (SES/HIGH), expiry }
    GGBS-->>INV: signature reference (+ signing URL)
    INV->>INV: persist signature_id + level on quote · Quote = Sent
    GGBS->>CLIENT: email signing link (ggbs-owned)
    CLIENT-->>GGBS: opens link in a tab (consult & sign)
    GGBS-->>INV: outcome viewed → Quote = Viewed
    alt Client signs
        CLIENT-->>GGBS: signs (SES or HIGH face-recognition)
        GGBS->>FILES: signed PDF + audit trail (statutory window → product archive promise; only the content hash is permanent)
        GGBS-->>INV: outcome signed { signed_at, audit-trail ref }
        INV->>INV: persist audit-trail ref + signed_at · Quote = Signed
    else Client declines
        GGBS-->>INV: outcome declined { reason? } · Quote = Declined
    else Signing window lapses
        GGBS-->>INV: outcome expired · Quote = Expired
    end
    Note over INV: Signed quote → explicit convert to Draft Invoice (doc 3, doc 4)

8. Sources

9. Related Documents

7. Reminders

Reminders

This document is the target-design specification for payment reminders on issued invoices and validity reminders on quotes — the manual and automatic nudges Green-Got sends so a customer’s clients pay (or sign) on time, and the rules that decide when no reminder may be sent.

1. Terminology

Terms shared across the documentation set are defined in the invoicing overview. This document adds:

2. Overview

Reminders are a Green-Got concern, not a B2Brouter or PA concern. The PA (B2Brouter) transmits the invoice and tracks its legal lifecycle; it does no dunning. Green-Got owns the relationship with the seller, owns the payment link and collection rails (8. Payment Link and Collection), and therefore owns reminding.

A reminder is a notification driven by Green-Got’s own state. It reads the issued invoice’s internal status (the projection of the AFNOR legal status — see 3. Invoice Domain and PA 5. Lifecycle Statuses) and the invoice’s payment/matching state (9. Transaction Matching), and decides whether to send.

There are two ways a reminder is produced:

Use caseTriggerRecipientCadence-driven?
Manual “Send reminder”An org user clicks “Send reminder” on an invoice or quote.Client (email), with a confirmation back to the acting org user.No — ad hoc, any time the invoice is unpaid and not in a skip state.
Automatic cadenceA scheduled offset is reached for an invoice/quote in a remindable state.Client (email) + org user (push).Yes — driven by the cadence in §4.

Design rule: A reminder never mutates the invoice’s AFNOR legal status or its internal status. Reminders are read-only with respect to lifecycle state; they only emit communications and record that a reminder was sent.

Design rule: Both use cases are subject to the same skip conditions. The manual button is a convenience that re-uses the automatic path’s eligibility check; it cannot send a reminder for a paid, cancelled, refused, or disputed invoice.

3. Reminder Use Cases

3.1 Manual “Send reminder”

An org user can send a reminder on demand from the invoice (or quote) detail view. This is the explicit dunning action the seller takes when they want to chase a specific client outside the automatic schedule.

3.2 Automatic cadence

Reminders are fully configurable per document. Before sending a quote or invoice, the customer chooses its reminder mode:

When the mode is automatic or custom, Green-Got evaluates each issued, unpaid invoice against that document’s cadence and sends reminders automatically as each offset is reached. The cadence is a sequence of offsets relative to the invoice’s due_at (§4).

Design rule: The reminder cadence is chosen per document by the customer before sending. The defaults in §4 are Green-Got’s recommended starting cadence, presented as suggestions and fully overridable — not a hard-coded schedule.

4. Default Reminder Cadence

The default cadence anchors on the invoice due date (due_at) and fires reminders before and after it. It is presented here as the recommended configuration; the customer may add, remove, or move offsets when they choose a custom cadence before sending the document.

StepOffsetRelative toTone / intentChannels
1D-33 days before dueFriendly upcoming-payment noticeEmail to client + push to org user
2D-dayOn the due dateDue-today reminderEmail to client + push to org user
3D+77 days overdueFirst overdue reminderEmail to client + push to org user
4D+1414 days overdueSecond overdue reminder, firmerEmail to client + push to org user
5D+3030 days overdueFinal overdue reminder; flag for manual escalationEmail to client + push to org user

Key characteristics:

flowchart LR
    Issued["Invoice issued
(due_at set)"] --> D3["D-3
upcoming notice"] D3 --> Due["D-day
due today"] Due --> D7["D+7
1st overdue"] D7 --> D14["D+14
2nd overdue"] D14 --> D30["D+30
final overdue
flag for escalation"] D30 --> Stop["No further automatic reminders"] Paid["Settled
(fully matched payment)"] Void["Cancelled / Refused / Disputed"] Issued -. "skip + stop cadence" .-> Paid Issued -. "skip step (may resume)" .-> Void

5. Channels

A reminder fans out to two distinct audiences over two channels:

ChannelAudiencePurposeCarries
EmailThe client (buyer)Ask the client to pay (invoice) or sign (quote).Invoice number, amount due, due date, and the payment link (8. Payment Link and Collection); for a quote, the signing link (6. Quote Signature).
Push / in-appThe org user (seller)Inform the seller that a reminder went out / that the invoice is overdue, so they can intervene.A reference to the invoice/quote and its current status.

Design rule: Only the client-facing email is a dunning communication; the org-user push is an awareness notification. The two are always emitted together for an automatic run, but a manual run primarily produces the client email plus a confirmation to the acting user.

Design rule: The client email reminder embeds (or links to) the payment link so the reminder is actionable. A reminder for an invoice that has no payment means is degraded; see the payment-link doc for how the link is generated and what it carries (8. Payment Link and Collection).

6. Skip Conditions

Before any reminder run sends, it evaluates the invoice’s current state. A run in a skip state sends nothing and is recorded as skipped.

ConditionInternal status / signalBehaviour
Paid in fullSettled (AFNOR Encaissée)Hard stop — the cadence terminates. No further reminders, automatic or manual. See §10. Invoices settle in full only — there is no partial-paid state.
Cancelled / voidCancelledSkip permanently — a cancelled invoice is not chased.
Refused by buyerRefused (AFNOR Refusée)Skip — the buyer rejected the invoice on business grounds; chasing payment is inappropriate until resolved (typically via a credit note / re-issue).
DisputedDisputed (AFNOR En litige)Skip while disputed; the cadence may resume if the dispute resolves and the invoice returns to an unpaid-but-accepted state.
Not yet issuedDraft / not finalizedNo cadence runs — reminders apply only to issued invoices.

Design rule: The decisive skip is settlement. Once an invoice reaches Settled (full payment matched, AFNOR Encaissée), all reminding ceases permanently. This is the load-bearing invariant of the reminder system (§10).

Design rule: Refused and Disputed are sourced from the AFNOR legal status projected onto the invoice via TransmissionStatusChanged (PA 10. Integration Contracts). Settled is sourced from Green-Got’s own matching (9. Transaction Matching) and is binary — an invoice is either fully settled or not settled at all. The reminder evaluator reads both.

Design rule — cadence resumes at the next due offset after a dispute, with no make-up. While an invoice is Disputed, every reminder run that falls in the dispute window is skipped (recorded as skipped, §9). When the dispute resolves and the invoice returns to an unpaid-but-accepted state, the cadence resumes from the next offset that is now due relative to the unchanged anchor (due_at) — it does not retroactively fire the offsets that were skipped during the dispute. Offsets that were skipped are gone; there is no catch-up burst. (Because each offset is anchored to due_at, not to “time since last reminder”, resumption is simply the normal sweep picking up the next not-yet-sent due offset — see §9.)

7. Relation to Overdue Status

“Overdue” is not an AFNOR status and not a stored internal status. It is a derived condition: an issued invoice is overdue when now > due_at and the invoice is not Settled, Cancelled, Refused, or fully credited.

Design rule: Reminder logic derives “overdue” at evaluation time from due_at and the current status; it does not persist an “Overdue” status that could drift out of sync with the legal lifecycle.

8. Quote Reminders

Quotes (4. Quote Domain) are reminded on a different anchor and for a different action: the goal is to get the quote signed (6. Quote Signature) before it expires, not to collect payment.

Design rule — offsets are measured from the document’s dates; viewed_at is informational only. Quote cadence offsets are computed from the quote’s own dates (the pre-expiry offsets from expires_at, which itself derives from issue_date + validity_days), never from when the client opened the quote. The quote’s viewed_at (4. Quote Domain §2.1) is informational — it records engagement for reporting but does not advance, reset, or suppress the cadence. View-based suppression (e.g. stop chasing once the client has viewed) is deliberately deferred: a viewed-but-unsigned quote is still chased on its schedule. The same principle applies to invoice reminders — offsets run off due_at (and issue_date for any pre-due step), and an invoice’s viewed_at is informational, not a cadence input.

AspectInvoice remindersQuote reminders
Anchor datedue_at (payment due)expires_at (validity end)
GoalGet the invoice paidGet the quote signed before expiry
Default cadenceD-3, D-day, D+7, D+14, D+30Pre-expiry offsets (e.g. D-7, D-3, D-1)
Skip onSettled / Cancelled / Refused / DisputedSigned / Cancelled / Expired
Client link in emailPayment link (8)Signing link (6)

9. Scheduling (Temporal)

The reminder scheduler is Temporal, already part of the Green-Got stack and the most reliable mechanism available. Two complementary mechanisms drive the cadence:

Design rule: Reminder runs are idempotent. A run records that the offset was handled, so a per-document timer and the daily sweep cannot double-send for the same offset, and a retried Temporal activity is harmless.

Design rule: Documents whose reminder mode is none schedule no timers and are skipped by the sweep; only manual reminders remain available for them.

10. Invariants

10. Related Documents

Payment Link and Collection

This document is the target-design specification for how a Green-Got customer’s client actually pays an issued invoice: the payment link Green-Got generates on its own rails, the collection lifecycle from link to paid invoice, and how a confirmed collection drives the AR-commercial Paid status (via TransactionMatched) and feeds payment-data e-reporting through the VAT-collection ledger (via PaymentCollected). The PaymentCollected ledger is the data source; for an invoiced domestic-B2B (Flux 1) operation the legally-effective payment-data report is the AFNOR Encaissée (212) status enriched with the MEN (driven by MarkOutboundInvoicePaid), while a separate Flux 10 payment-data flow applies only to non-invoiced ops — see PA 7. E-Reporting §6 and PA 5. Lifecycle §13. Encaissée is reached via MarkOutboundInvoicePaid, never via TransactionMatched.

1. Terminology

Terms shared across the documentation set are defined in the invoicing overview. This document adds:

2. Why Payment Is Green-Got’s Responsibility

B2Brouter does no payment processing or settlement. It is an invoicing and compliance platform: it generates the Factur-X / UBL / CII artefact, routes it over Flux 1, tracks the AFNOR legal lifecycle, and handles e-reporting. Its payment fields (payment_method, iban, remittance_information, creditor_reference, payment_terms) are metadata only, and mark_as paid is a status flag — none of them move money. See PA 10. Integration Contracts §8 and the B2Brouter research (payment is explicitly out of scope for the PA).

Therefore collection is Green-Got’s job. Green-Got owns the seller relationship, the seller’s bank account, and the inbound transaction stream, so it is the only party that can:

  1. present the client a way to pay (the payment link), and
  2. observe that the payment actually arrived (transaction matching),

and then drive the invoice to AR-commercial Paid. The PA learns about collection only because Green-Got tells it: settlement via TransactionMatched and the reportable VAT-collection facts via PaymentCollected, from which plateforme_agreee derives the payment-data overlay (carried by the enriched 212/MEN for invoiced Flux-1 ops, Flux 10 only for non-invoiced ops — D-CARRIER).

Design rule: Settlement of an issued invoice is computed from Green-Got-owned payment data, never from B2Brouter. The paid B2Brouter status and AFNOR Encaissée are outputs of Green-Got’s collection, set by Green-Got, not signals received from the PA.

3. The Payment Link

A payment link is a hosted means tied to one issued invoice. It lets the client pay without a Green-Got account and gives Green-Got a deterministic way to reconcile the resulting credit.

3.1 What the link carries

ItemSourcePurpose
Invoice number + issuer identityThe issued invoice (3. Invoice Domain)Identifies what is being paid; shown to the payer.
Amount dueInvoice totalThe full amount to collect. Invoices settle in full only — a payment link always asks for the entire invoice total, never a partial or remaining amount.
CurrencyInvoice currency (BT-5)The settlement currency.
Structured payment referenceGreen-Got-generated, embedded as the remittance/creditor referenceThe key that lets an inbound bank credit be matched to this invoice (9. Transaction Matching).
IBAN / remittance detailsThe seller’s collection accountWhere the money goes (when the rail is a transfer).
Due dateInvoice due_atShown to the payer; ties to reminders (7. Reminders).

Design rule: The payment link references the issued invoice by id and reads its (immutable) data live; it does not embed its own flat-file copy of the invoice PDF. A display preview shown alongside the link MAY be regenerated on the fly from the locked invoice data. But the legal/auditor/customer archive download serves the stored legal artifact (legal_artifact_ref) — the exact transmitted / PA-generated Factur-X / UBL / CII, content-hashed and stored permanently. Regeneration is a display/fallback convenience only, never the compliance copy (see 3. Invoice Domain §4).

Design rule: The same structured payment-means fields the link carries are the EN 16931 payment-means fields (BG-16: means code BT-81, IBAN BT-84, remittance/creditor reference) that go on the e-invoice itself. They are defined once in PA 4. Formats and Invoice Data §4 and are not redefined here. The link and the invoice carry the same reference so that a payment made via the link and a payment made by reading the invoice both reconcile identically.

Invariant: The payment reference is stable per invoice and unique enough that an inbound credit quoting it matches exactly one invoice. It is the primary exact-match key in 9. Transaction Matching §3.

3.2 Relationship to reminders

The payment link is the actionable element embedded in client-facing reminder emails (7. Reminders §5). A reminder without a payment link is degraded; the link is what turns a reminder into a one-click path to pay.

4. Collection Lifecycle

Collection runs from link creation through to a settled invoice. The link itself is the front door; the actual settlement is decided by matching an inbound bank transaction to the invoice.

sequenceDiagram
    autonumber
    participant Seller as Org user (seller)
    participant INV as invoicing (AR)
    participant Client as Client (buyer)
    participant Bank as Bank / payment rail
    participant Match as Transaction matching
    participant PA as plateforme_agreee

    Seller->>INV: Issue invoice → request payment link
    INV->>INV: Generate payment link (amount, reference, IBAN/remittance)
    INV-->>Client: Share link (reminder email / direct share)
    Client->>Bank: Pay the full amount via the link (rail = SEPA Instant by default, §6)
    Bank-->>Match: Inbound credit transaction arrives (carries the reference)
    Match->>Match: Reconcile credit ↔ invoice (exact via reference)
    alt Transaction covers the full invoice amount
        Match->>INV: Mark invoice Paid (AR-commercial)
        INV-->>PA: emit TransactionMatched { invoice_id, transmission_id, amount, currency, date }
        Note over PA: AR commercial status → Paid (settlement only)
    else Transaction does NOT cover the full amount
        Match->>INV: No settlement — surface as an anomaly to the org user
        Note over INV: Invoice stays unpaid; never a partial state
    end
    Match->>INV: Record the real collection in the VAT-collection ledger
    INV-->>PA: emit PaymentCollected { invoice_id, transaction_id, allocated_amount, vat_breakdown, collection_date, currency, ... }
    Note over PA: Derives payment-data overlay (212/MEN invoiced; Flux 10 non-invoiced) when VAT is on encaissement

Design rule — the link settles in full only; the ledger records what the bank actually did. Two things are deliberately separated:

The states a collection passes through:

StageWhat happenedInvoice internal status
Link createdPayment link generated for the issued invoice (full amount).Issued … (unchanged by link creation)
Client paysThe client pays the full amount through the link/rail.Unchanged until the credit is observed.
Bank transaction arrivesAn inbound credit appears on the seller’s Green-Got account.Unchanged until matched.
Matched (full amount)The credit covers the full invoice amount.AR-commercial Paid (the AFNOR Encaissée 212 legal status / Settled projection is reached separately via MarkOutboundInvoicePaid, never via this match)
Does not cover full amountAn inbound credit cannot settle the invoice in full.Unchanged — surfaced to the org user; no partial invoice status. The credit is still recorded in the payment-allocation ledger (§5.1).

Design rule: Creating or sharing a payment link does not change the invoice’s status. Only a full-amount matched inbound transaction moves the invoice to AR-commercial Paid. The link is a means; matching is the source of truth for collection. The matching mechanics — exact vs fuzzy, full-amount match, manual override, and the anomaly path — live in 9. Transaction Matching.

5. Confirmed Payment Drives Encaissée and E-Reporting

When matching confirms a full collection, two distinct facts are emitted — settlement and VAT collection — and they drive two distinct things:

  1. Settlement. Green-Got marks the invoice AR-commercial Paid and emits TransactionMatched to plateforme_agreee{ invoice_id, transmission_id, amount, currency, collection date } per the PA 10 event contract (the amount always equals the full invoice total; there is no partial variant). This drives the AR commercial lifecycle to Paid only; it is not the trigger for payment-data e-reporting. (Settled is reserved for the AFNOR legal status and the coarse business_api projection — see 3. Invoice Domain §3.)
  2. VAT collection. Green-Got records the real bank-level collection in the payment-allocation / VAT-collection ledger (§5.1) and emits PaymentCollected. plateforme_agreee consumes it and — when VAT is due on collection (encaissement)derives the payment-data overlay from that ledger entry: PaymentCollected is the data source; the carrier is the enriched 212/MEN for invoiced Flux-1 operations, and Flux 10 only for non-invoiced operations (D-CARRIER, see PA 7. E-Reporting §6).
  3. Legal status. The AFNOR Encaissée (212) legal status, where it applies, is reached via MarkOutboundInvoicePaid, never via TransactionMatched. The PaymentCollected ledger is the data source for the payment-data report; the carrier is channel-specific (D-CARRIER) — for an invoiced Flux-1 op it is the enriched 212/MEN (so the enriched Encaissée status is the payment-data carrier, not a side channel; the bare status transition is not itself the report), and a separate Flux 10 payment-data flow applies only to non-invoiced ops. See PA 7. E-Reporting §6.

Design rule: Payment-data e-reporting (collection date, amount collected incl. VAT, VAT amount per rate, currency if not EUR) is derived from PaymentCollected (the VAT-collection ledger entry), and only when VAT is on encaissement. It is not emitted when VAT is on debits, for reverse-charge operations (the buyer accounts for VAT), or when already covered by B2C transaction data. The chargeability regime carried on the canonical structured invoice governs this — see PA 7. E-Reporting §5 and 10. Tax and VAT. B2Brouter performs the Flux 10 transmission automatically for a DGFiP-enabled account; Green-Got makes no separate call to DGFiP.

Invariant: Both TransactionMatched and PaymentCollected are Green-Got-internal (invoicing → plateforme_agreee). B2Brouter never observes the payment directly; it learns of collection only through the e-reporting overlay plateforme_agreee derives from PaymentCollected.

5.1 The payment-allocation / VAT-collection ledger

TransactionMatched is the commercial AR match: it answers “is this invoice paid in full?” and drives the AR-commercial Paid status. It is not the record of what the bank actually collected, and it is not the trigger for payment-data e-reporting. The bank-level truth — partials, overpayments, refunds, bank fees, manual rematches, and corrections — is recorded in a separate payment-allocation / VAT-collection ledger, defined canonically in PA 10. Integration Contracts §11 (and referenced from PA 12. Data Model). This doc does not redefine it; it records that it exists and why invoicing relies on it.

Design rule — this ledger is AR-only; AP settlement is separate. The payment-allocation / VAT-collection ledger is owned by invoicing and keyed on invoice_id only — it has no bill_id and never references an AP document. Outbound supplier-payment settlement (the AP side) is reconciled entirely in bills (PA 10 §2): it carries no Flux 10 seller-side payment data, emits no PaymentCollected, and there is no cross-link between this AR ledger and any AP reconciliation record. Buyer-side reportable acquisitions, where they apply, are a distinct bills-owned record, not an entry here.

Ledger fieldPurpose (referenced, not redefined)
invoice_idThe AR invoice the collection is allocated against. The payment-allocation ledger is seller-side AR-only, keyed on invoice_id; there is no bill_id.
transaction_idThe real bank transaction observed on Green-Got rails.
allocated_amountThe amount of this transaction allocated to this document — may be partial.
vat_breakdownList of breakdown groups, each { vat_rate, vat_chargeability, taxable_base, vat_amount, reportability_state } — grouped by both rate and chargeability regime, with a per-group reportability_state. The substrate for the payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops). Shape defined canonically in PA 10 §11.1.
collection_date, currency, sourceWhen/how the collection occurred and where the record came from.
correction_reversal_linkLinks a correcting / reversing entry to the entry it amends (refunds, rematches).
cumulative_collected_amountRunning total collected against the document across entries.

Design rule — two distinct facts, two distinct records. The AR commercial match (TransactionMatched, full-amount-only → AR-commercial Paid) and the payment-collection / allocation record (one ledger entry per real bank-level collection event) are separate. The invoice’s status is driven by the former; the payment-data e-reporting is fed by the latter. A SEPARATE payment-collection event (referenced generically — see PA 10 §6 PaymentCollected) feeds the ledger and payment-data reporting; TransactionMatched does not. The full-amount-only link constrains settlement; it does not constrain what the ledger may record, which is whatever the bank actually moved.

Invariant: A non-settling credit (partial, overpayment, refund, bank fee) changes no invoice status and emits no TransactionMatched, but it is recorded in the payment-allocation ledger and may carry its own reporting obligation. The ledger is the source of truth for VAT-on-collection reporting; the invoice status is the source of truth for the AR commercial lifecycle.

Design rule — mixed-invoice collections report only the encaissement groups. A collection that pays a mixed invoice (goods on débits + services on encaissement, possibly at the same rate) must report payment data only for the encaissement groups. The canonical ledger achieves this by grouping vat_breakdown on { vat_rate, vat_chargeability } with a per-group reportability_state, so a goods group at 20 % stays not_reportable while a services group at 20 % is reportable. invoicing does not redefine this — it defers to PA 10 §11.1. Worked example: full €1,800 TTC collected on goods €1,000 HT/€200 VAT (débits) + services €500 HT/€100 VAT (encaissement) yields two breakdown groups; plateforme_agreee reports only the services group (€500 / €100) over the channel-specific payment-data carrier (Enriched212Men for this invoiced Flux-1 invoice), the goods VAT being chargeable at issuance (CGI art. 269), never at collection. See the full worked example in PA 10 §11.1 and 10. Tax and VAT §5.

6. The Payment Rail

Payment is initiated on Green-Got’s own rails, following the same pattern as the bills (AP) side (bills 3. Payment and Matching §2):

Design rule — fraud / AML may make instant unavailable and may block the transfer. As on the bills side, the payment is subject to Green-Got’s risk / scoring engine. For AML and fraud reasons it may:

Design rule: Whatever rail or timing applies, the collection model above is invariant: a payment link for the full amount carrying a stable structured reference, an inbound transaction observed on Green-Got rails, full-amount reconciliation in 9. Transaction Matching, TransactionMatched driving the AR-commercial Paid status, and PaymentCollected feeding the VAT-collection ledger from which the payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops) is derived. The rail and timing change how and when the credit is initiated, not the settle-in-full contract.

7. Invariants

8. Related Documents

9. Transaction Matching

Transaction Matching

This document is the target-design specification for reconciling an inbound bank transaction to an issued (AR) invoice: the matching strategies, the match-link data model, the anomaly path for transactions that do not cover the full amount, how a full-amount match drives the AR commercial Paid status and emits TransactionMatched, and how that commercial match is kept distinct from the separate payment-collection / VAT-collection event (PaymentCollected) that records real bank-level collection in the payment-allocation ledger and is the internal data source for payment-data e-reporting (the output carrier is Enriched212Men for invoiced Flux-1 operations, and Flux10 only for non-invoiced operations — see PA 7. E-Reporting §6 and §6.1).

1. Terminology

Terms shared across the documentation set are defined in the invoicing overview. This document adds:

2. Overview — The Matching Problem

Green-Got owns collection on its own rails (B2Brouter does no settlement — see 8. Payment Link and Collection §2). So when a client pays, the money lands as an inbound credit on the seller’s Green-Got account, decoupled from the invoice. The matching problem is to reconcile that inbound credit back to the issued invoice it pays, so the invoice can be marked paid, the seller’s books are accurate, and (when VAT is on encaissement) the collection can be e-reported.

The problem is non-trivial because:

Matching therefore combines a deterministic exact path with a user-mediated fuzzy path, settles only on a full-amount match, and always records how a match was made.

Design rule: Matching runs on Temporal (already in the Green-Got stack). Inbound credits are matched as they arrive, and a daily Temporal sweep over Postgres re-evaluates unmatched credits and open invoices so a missed exact match or a late-arriving transaction is still reconciled (or surfaced as an anomaly).

Design rule: Matching is an invoicing (AR) concern, distinct from the bills (AP) reconciliation of outbound supplier payments (PA 10 §2). AR matching reconciles inbound credits to issued invoices; AP reconciliation reconciles outbound debits to received bills. They do not share a table.

3. Matching Strategies

StrategyKeyConfidenceAuto-applied?
Exact (reference)Transaction remittance/reference contains the invoice’s structured payment referenceHigh — deterministicYes (source = Auto) when it resolves to exactly one invoice
Fuzzy (amount + counterparty)Full invoice amount + counterparty identity + timing windowLower — heuristicNo — proposed to the org user, applied as source = Manual on confirmation
ManualUser explicitly links a transaction to an invoiceAuthoritativeApplied as source = Manual

3.1 Exact match

The primary strategy. Green-Got embeds a stable, unique structured reference on each invoice’s payment means and payment link (8. Payment Link and Collection §3). When an inbound credit’s remittance information contains that reference, the transaction maps to exactly one invoice and is matched automatically (source = Auto).

Design rule: The exact-match reference is the same EN 16931 remittance/creditor reference defined in PA 4. Formats and Invoice Data. Because the payment link, the e-invoice, and the matcher all use one reference, a payment made by reading the invoice and a payment made via the link reconcile identically.

3.2 Fuzzy match

When no reference resolves a transaction, Green-Got proposes candidate invoices ranked by amount equality (against the invoice’s full total), counterparty match, and proximity in time. These are proposals, surfaced to the org user, never auto-applied. The user confirms or rejects; a confirmed proposal becomes a Manual match. A confirmed match still settles the invoice only if it covers the full amount (otherwise it is handled as an anomaly — §5).

Design rule: A fuzzy candidate is never settled automatically. Ambiguity (e.g. two open invoices with the same amount for one counterparty) is resolved by the user, not by the system guessing.

4. Matching Flow

flowchart TD
    Tx["Inbound credit transaction arrives
(Green-Got rails)"] --> Ref{"Remittance contains
invoice reference?"} Ref -->|"yes → unique invoice"| Auto["Exact match
source = Auto"] Ref -->|"yes → ambiguous / no"| Fuzzy["Fuzzy candidates
amount + counterparty + timing"] Fuzzy --> Propose["Propose to org user"] Propose -->|"user confirms"| Manual["Manual match
source = Manual"] Propose -->|"user rejects / none"| Unmatched["Leave unmatched
(await another tx or manual link)"] Auto --> Amount{"Transaction covers
full invoice amount?"} Manual --> Amount Amount -->|"yes"| Full["Create match link
Invoice → Paid (AR commercial)"] Amount -->|"no"| Anomaly["No settlement
surface anomaly to org user"] Full --> Ev["emit TransactionMatched
(commercial match → Paid)"] Full --> Ledger["Record payment-allocation ledger entry
emit PaymentCollected"] Ledger --> Flux["PA: payment data
(Enriched212Men for invoiced;
only if VAT on encaissement)"]

The two arrows out of the full-amount match are deliberately separate: TransactionMatched drives the commercial status, while the ledger entry / PaymentCollected is what feeds payment-data reporting. Neither, on its own, sets the AFNOR legal status — see §6 and §6.1.

5. No Partial Payment — Full Amount Only

An invoice settles in full only via the payment link. There is no outstanding balance, no instalment plan, and no partial-paid commercial state reachable through the link. The customer-facing payment link is full-amount-only: it is always for the entire invoice amount, so a payer who lacks the funds simply cannot pay it through the link.

There is deliberately no instalment, overpayment, or underpayment modelling in the commercial-match path: any inbound credit either matches an invoice’s full amount (settles it) or does not (anomaly).

Design rule — the settlement target is net of applied credit. When a credit_applied avoir has been imputed onto an invoice via the non-bank credit-allocation construct (3. Invoice Domain §5.5), the invoice’s settlement target is invoice TTC − Σ(applied credit), not the gross TTC. A full-amount match is then a bank transaction covering the net target, which — combined with the non-bank credit allocation — sums to the full invoice total. The full-amount-only rule is unchanged: the invoice settles only when (bank credit + applied avoir credit) equals the full total; a bank credit short of the net target is still an anomaly. The applied credit moves no money and never appears in the bank payment-allocation ledger (§6.1); it only lowers the cash the buyer must transfer. A bank credit that exactly covers the net target therefore drives the commercial status to Paid and emits TransactionMatched for the full invoice total (the event amount remains the gross total; the split between cash and imputed credit is recorded on the ledger entry + the CreditAllocation).

Design rule: Commercial settlement is decided by whether a transaction covers the invoice’s full total. There is no cumulative commercial accrual and no “partially paid” status — AFNOR has no partial-payment code (AFNOR 206 is partial approval, not partial payment).

Design rule — the link is full-amount-only, but the ledger records what the bank actually collected. The full-amount-only rule governs the payment link and the commercial match, not the platform’s record of real money movement. The payment-allocation / VAT-collection ledger (PA 10 §11) records whatever the bank actually collected — including a partial transfer made outside the link, an overpayment, bank fees, a refund/chargeback, a manual rematch, or a post-report correction. That ledger is not part of the commercial-match path described in this section: the link still rejects partials commercially, while the ledger faithfully captures the messy bank reality for VAT-on-collection reporting. See §6.1.

Invariant: A settling commercial match is a single transaction covering the full invoice amount; an invoice is matched to exactly one settling transaction. A credit that does not cover the full amount produces no settling match link and is raised as an anomaly. (The payment-allocation ledger may still record bank-level collection events for the same invoice independently — §6.1 — but those are not commercial match links.)

6. On Full-Amount Match — Paid, Event, and the Collection Ledger

When a transaction covers the invoice’s full amount, the commercial match runs:

  1. The invoice is marked Paid — the AR commercial terminal paid status (3. Invoice Domain §3). This is the AR commercial lifecycle, not the AFNOR legal status.
  2. invoicing emits TransactionMatched to plateforme_agreee: { invoice_id, transmission_id, amount, currency, collection date } (PA 10 §6). The amount always equals the full invoice total; there is no partial variant of the event.
  3. The corresponding AFNOR Encaissée (212) legal status, where it applies, is reflected back as a projection from plateforme_agreee via TransmissionStatusChanged (3. Invoice Domain §6). TransactionMatched does not itself write the legal status: collection and the legal lifecycle are distinct axes (PA 10 §7.1).

Separately, the real bank-level collection is recorded in the payment-allocation ledger and feeds payment-data reporting — see §6.1.

Invariant: The AR commercial Paid status is set by invoicing from the match, never received from B2Brouter (which does no settlement). The PA learns of collection only via the Green-Got-internal events; the AFNOR Encaissée legal status is a projection reflected back, not a value invoicing authors.

6.1 Two events — commercial match vs payment collection

The full-amount match and the recording of real bank-level collection are two distinct events on two distinct axes, deliberately not collapsed into one. This is the same separation the integration contract defines authoritatively in PA 10 §6 and PA 10 §7; invoicing must keep them apart on the AR side too.

AspectTransactionMatched (commercial match)PaymentCollected (payment collection / allocation)
What it representsThe AR commercial match: issued invoice ↔ incoming bank transaction, paid in full via the link.One real bank-level collection allocated to the document, with a VAT breakdown.
What it drivesThe AR commercial status → Paid (§6).The internal payment-data source when VAT is on encaissement; PA carries it as Enriched212Men for invoiced Flux-1 operations (Flux10 only for non-invoiced) (PA 7 §6).
Record writtenThe match link (§8).One entry in the payment-allocation / VAT-collection ledger (PA 10 §11, PA 12 §3).
Amount semanticsAlways the full invoice total; full-amount-only, no partial variant.Whatever the bank actually collected — partials, overpayments, bank fees, refunds/reversals, manual rematches, corrections.
Effect on AFNOR legal statusNone directly. Encaissée is reflected back separately, reached via MarkOutboundInvoicePaid.None directly. The ledger is the payment-data source (carried as Enriched212Men for invoiced operations), not the legal lifecycle.

Design rule — the commercial match is not the payment-data trigger. TransactionMatched drives the AR commercial lifecycle; it is not what feeds payment-data e-reporting. A dedicated payment-collection / allocation event (PaymentCollected) records each real bank-level collection in the payment-allocation / VAT-collection ledger and is the internal data source for payment data when VAT is on encaissement — the PA carries it as Enriched212Men for invoiced Flux-1 operations and Flux10 only for non-invoiced operations (PA 7 §6). The ledger is seller-side AR-only, keyed on invoice_id (no bill_id). Its fields (invoice_id, transaction_id, allocated_amount, vat_breakdown, collection_date, currency, source, correction_reversal_link, cumulative_collected_amount) and the append-only correction/reversal semantics are defined generically and canonically in PA 10 §11 and PA 12 §3 — this doc does not redefine them. Crucially, vat_breakdown is a list of groups keyed on { vat_rate, vat_chargeability }, each with its own reportability_statenot a per-entry state — so a mixed-invoice collection reports payment data only for the encaissement groups and never over-reports the goods (débits) VAT at the same rate.

Worked example — mixed invoice over one transaction. A full-amount match settles an invoice of goods €1,000 HT/€200 VAT (débits) + services €500 HT/€100 VAT (encaissement), both at 20 %. The single PaymentCollected entry carries a two-group vat_breakdown: the goods group (not_reportable) and the services group (reportable). Because this is an invoiced Flux-1 operation, plateforme_agreee carries the payment data on the enriched AFNOR Encaissée / 212 MEN (Enriched212Men), reporting only the services group — see the canonical worked example at PA 10 §11.1 and the goods-chargeable-at-issuance rule in 10. Tax and VAT §5.

Design rule — the link rejects partials, the ledger records reality. The payment link is full-amount-only and the commercial match settles in full only, but the platform records whatever the bank actually collected in the ledger: a partial transfer made outside the link, an overpayment, deducted bank fees, a refund or chargeback, a manual rematch to a different invoice, or a post-report correction. The commercial-match path stays clean (full-amount-only, one settling match link); the ledger absorbs the real-world mess for accurate VAT-on-collection reporting.

Design rule — payment data follows only the encaissement regime. Payment-data e-reporting follows only when VAT is on encaissement; it is not emitted for VAT-on-debits, reverse-charge, or amounts already in B2C data (PA 7 §5, 10. Tax and VAT). The ledger entry’s reportability_state records the outcome. For an invoiced Flux-1 operation the carrier is the enriched AFNOR Encaissée / 212 MEN (Enriched212Men), not Flux10 (PA 7 §6); Flux10 carries payment data only for non-invoiced operations. Green-Got makes no direct DGFiP call.

Invariant: Neither TransactionMatched nor PaymentCollected changes the invoice’s AFNOR legal status by itself. The AR commercial Paid status (from the match) and the bank-level collection ledger (from PaymentCollected) are distinct from the reflected AFNOR Encaissée legal status, which is reconstructed from B2Brouter/CDAR and projected back via TransmissionStatusChanged (PA 10 §7.2).

7. Manual Override

The org user can always override automatic matching:

Design rule: A manual override (including an unmatch) re-derives the invoice’s commercial status from its match link: a full-amount match means Paid, its removal means not paid. If the outcome changes, invoicing emits the corresponding TransactionMatched correction so the AR commercial status stays consistent with the match link. The corresponding bank-level correction (a rematch to a different invoice, a reversal/chargeback, or a post-report change) is recorded separately as an append-only payment-allocation ledger correction and re-emitted as PaymentCollected, which keeps payment-data reporting consistent (PA 10 §11.2). The system never leaves the commercial status out of sync with the match link, and never edits a reported collection in place — corrections are appended.

7.1 The anomaly / needs-review surface

A credit that does not settle an invoice (a partial, an overpayment, an unmatched credit, a currency mismatch) is not discarded — it is surfaced to the org user as a needs-review item so it can be resolved manually. The anomaly is not a parallel ledger: the partial/overpayment is already persisted in the payment-allocation ledger (§6.1). The needs-review surface is a read view over (a) unmatched inbound credits and (b) ledger entries that do not correspond to a settling commercial match, plus the invoice (if any) they plausibly relate to.

API. business_api exposes a list endpoint for the needs-review surface (e.g. invoicing.matching.needs_review.list) returning each anomalous credit with its candidate invoice(s) and the reason (under_amount | over_amount | currency_mismatch | no_candidate). The bounded resolution actions an operator may take are:

Design rule — no force-partial-settle. There is deliberately no action that settles an invoice for less than its (net) full total. The operator can link, dismiss, or escalate, but cannot create a partial-paid commercial state — the full-amount-only invariant holds. Every resolution action is recorded for audit; the underlying ledger entry is never edited (corrections are appended).

8. Match-Link Data Model

The match link is the persisted reconciliation record. It associates one issued invoice with one bank transaction.

FieldTypeMeaning
idprefixed time_sortable_idMatch-link identifier.
invoice_idissued-invoice id (inv_…)The AR invoice being settled.
transaction_idbank-transaction idThe inbound credit transaction (Green-Got rails / core_banking).
matched_amountinteger cents (i64)The settled amount; equals the invoice’s full total (a match links only on the full amount).
currencyISO 4217Currency of the matched amount; must equal the invoice currency for a settling match (see 10. Tax and VAT).
sourceenum Auto | ManualHow the match was made — Auto for exact reference, Manual for confirmed-fuzzy and manual links/overrides.
collection_datedateThe date the payment was collected; carried in TransactionMatched and used for payment-data reporting.
created_attimestampWhen the match link was recorded.

Design rule: The relationship is one issued invoice ↔ at most one settling match link ↔ one transaction. A settled invoice has exactly one match link covering its full total; an unpaid invoice has none. The match link is owned by invoicing; the bank transaction lives in the Green-Got banking substrate (core_banking).

Design rule — currency mismatch is rejected, never converted. A settling match requires transaction.currency == invoice.currency. A transaction whose currency differs from the invoice is never treated as an exact match and is never FX-converted to force one; it is surfaced as an anomaly to the org user. The future FX-snapshot rule (snapshot the rate at collection date on the PaymentCollected ledger entry, report the original currency over the payment-data carrier) is defined in 10. Tax and VAT §7; while invoices are EUR-only the mismatch case is simply rejected.

9. Invariants

10. Related Documents

Uncertainties

Invoicing — Active Register

This is an active, classified register of the open items in the invoicing (AR) domain. The AR business model is settled and documented (payment rail, status set, credit notes, numbering, quote→invoice conversion, signature, reminders, currency, VAT); this register tracks the remaining wire, legal/staging and product-decision items until each is closed.

Status sections. Items move through: §1 Research pending (delegated to Claude to investigate, then close), §2 Resolved decisions (decided here; awaiting port into the named canonical doc).

Where wire-level e-invoicing values live. Transport-layer wire values (B2Brouter field values, status/format transmission detail) are tracked in the transport register plateforme_agreee/docs/uncertainties.md. This register holds only items genuinely owned by the AR domain.

Convention. “Launch-blocking = yes” means outbound transmission for a real customer cannot proceed until the item is closed. A resolved item that is launch-blocking stays launch-blocking until ported into its canonical doc.


1. Research pending (delegated to Claude)

You asked me to research these, document the answer, and then close them. They remain open until I return grounded findings; I will document the answer in the named canonical doc and move the item to §2.

AR-1 — Structured legal-note BT-21 exact codes

Question: The exact UNCL4451 code-to-mention binding transmitted in the EN 16931 BT-21 structured note. The concept and the three subject codes are documented (PMT / PMD / AAB for recovery fee, penalties, escompte); only the precise binding remains open, pinned at the format-mapper against EN 16931 / FNFE-MPE and B2Brouter staging.

AR-2 — Invoice-identifier 20-char flux rule

Question: Must the rendered invoice identifier ({prefix}-{year}-{sequence}) satisfy a 20-character / charset constraint from the flux / Annuaire identifier rules — and if so, how does the numbering scheme guarantee it?


2. Resolved decisions (awaiting port into canonical docs)

AR-3 — Expired quote: no acceptance, re-issue instead

Decision (⚠️ reverses the prior §5.1 design). An expired quote cannot be accepted or signed — expiry is terminal for signing. Instead, the system notifies the customer and offers to re-issue a new quote based on the expired one (the new quote is then sent for signature afresh). There is no direct conversion / late-acceptance of an expired quote.

AR-4 — business_api DTO migration

Decision. Do it ASAP, as part of building the invoicing feature (not deferred post-MVP): migrate the coarse business_api Invoice/Quote/Client DTOs to the full canonical model / add the rich canonical fields needed to be transmissible, within the feature build.


3. Closing items

An item is closed by recording the confirmed value/decision in the owning AR doc; for staging-wire items, by additionally pinning the value in the format mapper against B2Brouter staging. AR-1 and AR-2 gate outbound transmission and are launch-blocking.

4. Related Documents

Plateforme Agréée

0. Documentation Index

Plateforme Agréée — Documentation Index

The plateforme_agreee crate is the shared transport + compliance layer for French e-invoicing. It integrates B2Brouter (a certified Plateforme Agréée / PA) and owns the regulatory model — formats, legal lifecycle statuses, annuaire routing, e-reporting, archiving, mandate/onboarding — consumed by both invoicing/ (issuing, AR) and bills/ (receiving, AP).

These documents are the source of truth: all e-invoicing code in this crate and its siblings is validated against them.

Foundation

Invoice model & lifecycle

Compliance

Onboarding & contracts

Architecture

B2Brouter API reference

Other

To Be Created

1. Reform Overview

Reform Overview

This document describes the French e-invoicing and e-reporting reform, its scope and rollout calendar, and Green-Got’s position in it as a Solution Compatible operating on top of the approved platform B2Brouter.

1. Terminology

This glossary is the canonical reference for acronyms and domain terms used across the entire Plateforme Agréée documentation set. Other docs reference it rather than redefining terms.

2. Reform Purpose and Legal Basis

The French e-invoicing reform makes structured electronic invoicing and transaction e-reporting mandatory for taxable persons (assujettis) established in France, including those that are not VAT-liable (franchise en base, micro-enterprises). Its objectives are to combat VAT fraud, narrow the VAT gap, simplify VAT compliance through pre-filled returns, and improve the real-time visibility of economic activity.

The reform splits the company’s obligations into two distinct regimes:

Design rule: Every in-scope French company must contract with at least one PA. There is no direct State portal a company can submit invoices to; the PPF is a directory and concentrator only. See 2. Platform Architecture.

The reform applies in the French overseas departments Guadeloupe, Martinique, and La Réunion. Guyane and Mayotte are VAT-extraterritorial zones (VAT temporarily not applicable), so operators established there are outside the e-invoicing scope — but they are not wholly excluded from the reform: when such an operator carries out operations deemed situated in France and subject to French VAT under CGI art. 290 II (e.g. sales to mainland France, imports subject to French VAT), those operations are in scope for e-reporting. The fix is “excluded from e-invoicing, e-reporting-relevant for France-located ops”, not “excluded from the reform”. Sources: impots.gouv.fr DROM-COM FAQ; Artéva DROM-COM analysis.

3. Scope — E-Invoicing vs E-Reporting

The boundary between the two regimes is defined by the nature of the counterparties and the transaction.

3.0 Who is in scope

The personal scope of the reform is every taxable person (assujetti) established in France, identified by its SIREN/SIRET and registered in the Annuaire. Scope follows the assujetti status, not VAT liability and not company size:

Design rule: Scope and routing key off the SIREN/SIRET + Annuaire entry, never off a VAT number. The buyer’s VAT number is a conditional invoice field (required only for certain operations — see 4. Invoice Formats and Mandatory Data), not the determinant of whether a party is in scope.

3.1 E-Invoicing (domestic B2B)

3.2 E-Reporting (B2C and cross-border)

Non-established (foreign) taxpayer e-reporting is deferred and phased. Taxpayers not established in France (no permanent establishment) that perform operations giving rise to a French e-reporting obligation must contract with a PA, but the obligation does not start on the standard date. It is phased: large enterprises and ETI selling goods or services from 2026-09-01; micro/VSE/SME sellers, and buyers/customers of any size liable for French VAT (reverse charge, non-exempt intra-EU acquisitions), from 2027-09-01. The non-established PA-choice deadline tracks the applicable date (2026-09-01 or 2027-09-01). In scope for non-established e-reporting: international B2B operations deemed situated in France, non-exempt intra-Community acquisitions in France, purchases in France from unregistered suppliers, B2C (unless registered for EU OSS), and payment data. Excluded: exports, intra-EU supplies, imports (VAT-exempt operations). Green-Got’s non-established enrollment/transmission code path must be gated on these 2026/2027 dates. Sources: impots.gouv.fr — E-reporting for foreign companies without PE; vatcalc — France e-reporting non-resident Sep-2027 warning.

Party identity is conditional by channel. The buyer SIREN/SIRET is mandatory only for domestic FR B2B (Flux 1), where it is the Annuaire routing key. For B2C and for foreign (cross-border) buyers (both Flux 10) there is no buyer SIREN to carry, and the buyer/seller VAT-number requirements follow the operation type, not a blanket rule. Channel classification (domestic FR B2B = Flux 1; B2C and cross-border B2B = Flux 10; B2G via Chorus Pro; Monaco/special) is determined before finalization, and which party identifiers are mandatory follows from it. See 4. Formats and Invoice Data §4.

3.3 Three-layer classification

It is tempting to treat the transaction channel (Flux 1 vs Flux 10) as the whole story. It is not. The reform stacks three independent layers, and a single operation can be touched by more than one. Conflating them produces the common error that “a domestic B2B invoice is never e-reported”.

Layer 1 — Transaction/invoice channel. Every operation lands in exactly one channel based on the counterparties:

Layer 2 — Payment-data overlay (CGI art. 290 A). Independently of Layer 1, payment data must be reported whenever VAT is due on collection (encaissement) — see 7. E-Reporting. This overlay sits on top of Layer 1: a domestic B2B service invoice (Flux 1) under the collection regime both flows as a structured invoice and generates a payment-data submission. The overlay does not apply where VAT is due on debits, or to reverse-charge operations (VAT due by the buyer — art. 290 A excludes “celles pour lesquelles la taxe est due par le preneur”).

Layer 3 — Provider implementation (B2Brouter). How the PA actually builds the tax reports, ledgers, status callbacks, and downloads that carry Layers 1 and 2 to DGFiP. See 2. Platform Architecture and 7. E-Reporting.

Invariant: Every invoice/payment event is classified twice and independently — once for transaction reporting (Layer 1: Flux 1, Flux 10, or out of scope) and once for payment-data reporting (Layer 2: required iff VAT is due on collection and the operation is not reverse-charge). The two classifications are orthogonal; a domestic B2B invoice (Flux 1) can also be payment-data-reported. The payment-data overlay feeds the payment-allocation / VAT-collection model in the PA lifecycle & integration docs.

3.4 Scope comparison

Transaction typeChannel (Layer 1)Payment-data overlay (Layer 2)
Domestic B2B goods (FR ↔ FR assujettis, VAT on debits)E-invoicing (Flux 1)Not required (VAT on debit)
Domestic B2B services (FR ↔ FR assujettis, VAT on collection)E-invoicing (Flux 1)Required on collection
Domestic B2B reverse-charge (buyer accounts for VAT)E-invoicing (Flux 1)Not required (excluded by art. 290 A)
B2C (sales to consumers)E-reporting (Flux 10)Required on collection (unless already in B2C cash data)
Cross-border B2B (intra-EU + extra-EU)E-reporting (Flux 10)Per art. 290 A / regime
B2G (public administration)Out of scope (via Chorus Pro)n/a

4. Rollout Calendar

The obligation is phased by company size, with reception obligations preceding issuance obligations. Reception applies to all in-scope French assujettis (every Annuaire-registered entity) from the first milestone.

4.1 Phases

Key characteristics:

4.2 Grace-period nuance

As of June 2026 there is no official delay to the reform. DGFiP has signalled there will be “no automatic and blind sanctions”, and a grace period for good-faith effort (extending to approximately January 2027 or longer) is taking shape. A decree could in principle shift the milestones (for example to 2026-12-01 / 2027-12-01), but none had been issued as of June 2026.

Design rule: Green-Got’s tooling must treat 2026-09-01 reception as the binding date for all customers and issuance as size-dependent. Do not assume any delay until a decree is published.

4.3 Calendar comparison

MilestoneDateWhoIssueReceiveE-reporting
Pilot window~2026-02-28 → 2026-08-31Voluntary participantsOptionalOptionalOptional
Phase 12026-09-01Large (>5,000 emp or >€1.5B) + ETI (250–5,000)MandatoryMandatoryMandatory
Universal reception2026-09-01All in-scope French assujettis (Annuaire-registered)Mandatory
Phase 22027-09-01PME (<250) + micro (<10 emp or <€2M)MandatoryMandatory (since 2026)Mandatory
Non-established sellers (large/ETI)2026-09-01Foreign taxpayers (no FR PE) selling goods/servicesn/an/aMandatory (e-reporting)
Non-established sellers (micro/VSE/SME) + VAT-liable buyers (any size)2027-09-01Foreign taxpayers (no FR PE): smaller sellers; all sizes as buyers liable for FR VAT (reverse charge, non-exempt intra-EU acquisitions)n/an/aMandatory (e-reporting)
Grace period~ to Jan 2027 or longerGood-faith effortNo “automatic and blind” sanctions

Non-established (foreign) taxpayers are a separate, phased e-reporting track (no permanent establishment in France). They have no e-invoicing issue/receive obligation; only e-reporting applies, phased to 2026-09-01 (large/ETI sellers) and 2027-09-01 (smaller sellers + VAT-liable buyers of any size). See §3.2. Sources: impots.gouv.fr; vatcalc.

5. Green-Got’s Role

Green-Got is a Solution Compatible (SC) — the software layer that lets its customers issue, receive, and reconcile invoices. Green-Got is not an approved platform. It relies on B2Brouter, a certified PA and Peppol Access Point, for all regulated transport and reporting.

Design rule: Green-Got never exchanges invoices or e-reporting data directly with the PPF, the Annuaire, or DGFiP. All regulated flows pass through B2Brouter (the PA). See 2. Platform Architecture and 3. Actors and Legal Posture.

What Green-Got provides on top of B2Brouter:

5.1 What Green-Got’s customers must do

6. Related Documents

7. Sources

10. Integration Contracts

Integration Contracts

This document defines the eventbus and data contracts between the three crates that implement Green-Got’s French e-invoicing: invoicing (accounts-receivable / issued invoices), plateforme_agreee (regulated transport and compliance via B2Brouter), and bills (accounts-payable / received supplier invoices). It is the authoritative source for who owns what, which events cross crate boundaries, and the single most important modeling rule: inbound (received) invoices belong to bills, never to invoicing.

1. Terminology

Terms shared across the documentation set are defined in 1. Reform Overview. This document adds:

2. The Three-Crate Responsibility Split

CrateOwnsDoes NOT own
invoicing (AR)Issued invoices and quotes; gap-free numbering; the canonical structured invoice for outbound; the AR-side projection of the legal lifecycle status; payment links; reminders; AR ↔ bank-transaction matching.The regulated transmission itself; the B2Brouter account; the authoritative legal status (it projects it); any received supplier invoice.
plateforme_agreee (transport)Transmissions (outbound + inbound); the B2Brouter HTTP adapter; mapping the canonical structured invoice → B2Brouter JSON; webhook ingestion + HMAC verification; the authoritative legal lifecycle status (mapped from B2Brouter/CDAR); the original inbound document download; the enrollment link (b2brouter_account_id).Issued-invoice authoring/numbering; AP bill lifecycle/approval/payment; the AR or AP business records (it transports, it does not author).
bills (AP)Received supplier invoices (Bill); supplier master; AP approval workflow; SEPA payment scheduling; AP ↔ bank-transaction reconciliation; OCR/structured parse of received documents.Outbound transmission; issued invoices; the legal transmission record (it consumes the inbound event and references the transmission, it does not own it).

Design rule: plateforme_agreee is a transport and compliance layer. It does not author business records. Outbound, it transmits what invoicing produced; inbound, it hands off to bills. The business records (AR invoice, AP bill) live in their owning crate; the transmission is a separate record that references them.

Design rule: A received (inbound) supplier invoice MUST NOT be foreign-keyed to invoicing.invoice. It is an accounts-payable document and belongs to bills. invoicing.invoice is exclusively AR (documents the customer issued). There is no FK, shared table, or shared Rust type between inbound documents and invoicing.invoice.

3. The Outbound Contract (AR → transport → legal status back)

3.1 Flow

  1. invoicing finalizes an issued invoice. Finalization is the point of gap-free numbering and immutability — a finalized invoice is the immutable AR record (corrections are credit notes / avoir, type 381; see 4. Formats and Invoice Data §6).
  2. invoicing emits InvoiceFinalized on its EVENT_BUS, carrying the invoice id and enough reference for plateforme_agreee to assemble the canonical structured invoice.
  3. plateforme_agreee (subscribed via a rules/ handler) maps the canonical structured invoice (4. Formats and Invoice Data) to B2Brouter’s JSON contract, creates a transmission record, and submits it to B2Brouter using the customer’s organisation-level b2brouter_account_id (resolved from the enrollment — see 9. Mandate and Onboarding §7).
  4. Channel is classified first (before finalization): domestic FR B2B → Flux 1; B2C and cross-border B2B → Flux 10 (e-reporting, not Flux 1 routing); B2G → Chorus Pro; Monaco/special → its dedicated path. The full classification rule is in 2. Platform Architecture. For the domestic FR B2B case described in this contract, B2Brouter generates the Factur-X / UBL / CII artefact and routes it via Flux 1 to the recipient’s PA. (B2C / cross-border do not take this Flux 1 routing path; they are reported over Flux 10 — see §7 and 7. E-Reporting.) See B2Brouter — Sending invoices.
  5. Legal lifecycle statuses (AFNOR XP Z12-012: Déposée, Rejetée, Refusée, Encaissée, plus recommended intermediates) flow back to plateforme_agreee via B2Brouter webhooks / CDAR. plateforme_agreee owns the authoritative status on the transmission. See 5. Lifecycle Statuses.
  6. plateforme_agreee emits TransmissionStatusChanged on each legal-status transition. invoicing consumes it and updates its projection of the status on the AR invoice (e.g. to drive UI, reminders, and overdue logic).

Design rule: invoicing reflects the legal lifecycle status as a projection, not as an owned field. The authoritative status lives on the plateforme_agreee transmission (mapped from B2Brouter + CDAR per the 3-layer mapping in 5. Lifecycle Statuses). invoicing never writes the legal status itself; it only mirrors what TransmissionStatusChanged tells it.

Invariant: One AR invoice maps to one outbound transmission (re-submission after a Rejetée correction issues a new transmission referencing the corrected invoice, never mutates the prior one). The transmission references invoicing.invoice; invoicing holds a back-reference (the transmission id and the projected status), but the transmission is owned by plateforme_agreee.

Design rule — domestic recipient absent from the Annuaire → REJECTION, never a Flux 10 fallback. For a domestic FR B2B outbound (Flux 1), the recipient must be discoverable in the Annuaire (PPF directory) so the invoice can be routed to its PA. If the recipient is not found in the Annuaire (no registered routing entry), the transmission terminates in a rejection state surfaced as AFNOR Rejetée (213) on the transmission, and TransmissionStatusChanged carries that status back to invoicing. The platform must NOT silently re-route a domestic unreachable invoice over Flux 10 — Flux-10-for-unreachable is a foreign-only mechanism and is non-compliant for a domestic operation that is legally a Flux 1 e-invoice. The buyer must be addressed via Flux 1 or the issuer must correct the recipient identifier and resubmit. The invoice number is preserved on resubmission: a corrected resubmission issues a new transmission referencing the same finalized AR invoice (same gap-free number), per the one-invoice-multiple-transmissions invariant above; the number is never consumed or renumbered by a routing rejection. The matching directory-lookup / not-found path on the routing side is defined in 6. Annuaire and Routing.

3.2 Diagram

sequenceDiagram
    autonumber
    participant INV as invoicing (AR)
    participant PA as plateforme_agreee
    participant B2B as B2Brouter (PA)
    participant Recip as Recipient PA (Flux 1)

    INV->>INV: Classify channel (domestic FR B2B = Flux 1; B2C / cross-border = Flux 10; B2G = Chorus Pro)
    INV->>INV: Finalize issued invoice (gap-free number, immutable)
    INV-->>PA: emit InvoiceFinalized { invoice_id, org_id, ... }
    PA->>PA: Build canonical structured invoice (EN 16931 → B2Brouter JSON)
    PA->>PA: Create transmission record (refs invoice_id)
    PA->>B2B: POST /accounts/{account}/invoices (org-level b2brouter_account_id)
    B2B->>B2B: Generate Factur-X / UBL / CII, validate
    B2B->>Recip: Route via Flux 1
    Recip-->>B2B: CDAR / lifecycle acknowledgements
    B2B-->>PA: Webhook: legal status (Déposée / Rejetée / Refusée / Encaissée …)
    PA->>PA: Map to authoritative legal status on transmission
    PA-->>INV: emit TransmissionStatusChanged { transmission_id, invoice_id, legal_status }
    INV->>INV: Update projected status on AR invoice (UI, reminders, overdue)

Key points:

4. The Inbound Contract (transport → AP bill)

4.1 Flow

  1. A supplier issues an e-invoice to the Green-Got customer. It is routed (Flux 1) to B2Brouter because the customer is Annuaire-registered to B2Brouter (see 9. Mandate and Onboarding).
  2. plateforme_agreee learns of the arrival. Polling is the authoritative inbound channel: plateforme_agreee polls GET /accounts/{account}/invoices (received list) / GET /invoices/{id} on a Temporal schedule and reconciles new received documents. Webhooks (X-B2Brouter-Signature: t=<ts>,s=<sig>, HMAC-SHA256 over "{ts}.{raw_json_body}", verified constant-time) are treated as an optimisation/notification hint that can trigger an earlier poll, not the system of record — unless B2Brouter staging proves webhook delivery reliable enough to be authoritative for the French inbound flow. Either channel is idempotent on the B2Brouter invoice id (duplicate deliveries are possible). See B2Brouter — Receiving invoices.
  3. plateforme_agreee persists an inbound transmission record and downloads the legal original document. Structured metadata/fields come from GET /invoices/{id}; the legal/original bytes that are archived (the supplier’s Factur-X / UBL / CII artefact) are fetched via GET /invoices/{id}/as/original. Any attachments[] are annexes / supplemental only — they are NEVER the legal invoice and must not be treated as the archived original.
  4. plateforme_agreee emits InboundInvoiceReceived on its EVENT_BUS, carrying the transmission id, the organisation id, references to the downloaded document, and the extracted structured fields.
  5. bills (subscribed via a rules/ handler) consumes InboundInvoiceReceived and creates the AP Bill in the Received state. Because PA-delivered invoices arrive with structured data, they need no OCR — but “structured source / no OCR needed” is not a Bill.status; it is recorded as extraction metadata outside Bill.status (a source / extraction field on the Bill). The Bill references the plateforme_agreee transmission (for the original document and legal trail), and the supplier master, but is owned by bills.

Design rule: plateforme_agreee stops at the inbound transmission — it persists the transmission and the original document and emits one event. It does not create or own the AP Bill. bills is the consumer that creates the AP document. This is the clean boundary the bills crate already describes (“PA stops at transmission received; bills picks up from there”).

Design rule: The inbound Bill is never linked to invoicing.invoice. There is no foreign key, no shared Rust type, and no shared table between received supplier invoices and issued invoices. AR and AP are separate domains; the only thing they share is the plateforme_agreee transport layer and the organisation/core_banking substrate.

Document storage. The original inbound artefact is stored in the core S3 bucket under the bills/ subpart and retained for the legal retention period (10 years accounting / 6 years VAT — see 8. Archiving §5). The inbound original cannot be regenerated, so it must be kept. The InboundInvoiceReceived event carries a document_ref pointing at the stored object, not the bytes; document_vault governs the retention horizon over it. Issued (outbound) invoices are likewise stored — Green-Got fetches the exact transmitted legal artefact (the PA-generated file via download_legal_url), content-hashes it, and keeps it as the authoritative legal archive; on-the-fly regeneration from the DB data is a display convenience only, not the compliance copy (see §3 and 8. Archiving §5.2).

4.2 Diagram

sequenceDiagram
    autonumber
    participant Sup as Supplier PA (Flux 1)
    participant B2B as B2Brouter (PA)
    participant PA as plateforme_agreee
    participant BIL as bills (AP)

    Sup->>B2B: Supplier e-invoice routed to customer
    Note over PA: AUTHORITATIVE PATH — scheduled polling / list-get reconciliation
    PA->>B2B: GET /accounts/{account}/invoices (received list, Temporal schedule)
    B2B-->>PA: Received list (new B2Brouter invoice ids)
    opt Webhook hint (accelerates, never authoritative)
        B2B-->>PA: Webhook (HMAC X-B2Brouter-Signature) — triggers an early poll
        PA->>PA: Verify signature; treat only as a hint to poll now
    end
    PA->>B2B: GET /invoices/{id} (structured metadata/fields)
    PA->>B2B: GET /invoices/{id}/as/original (legal/original bytes to archive)
    B2B-->>PA: Structured data + legal original Factur-X / UBL / CII (attachments[] = annexes only)
    PA->>PA: Dedupe on B2Brouter invoice id; persist inbound transmission + archive legal original
    PA-->>BIL: emit InboundInvoiceReceived { transmission_id, org_id, doc_ref, extracted_fields }
    BIL->>BIL: Create Bill (Received state), ref transmission + supplier
    Note over PA,BIL: PA owns the transmission; bills owns the Bill. No FK to invoicing.invoice.

Key points:

Design rule — inbound (received) status involves four separate axes. A received French invoice has four distinct states, never one enum:

AxisOwnerNotes
Green-Got bill payment statebills (AP)whether Green-Got has scheduled/made payment of the supplier bill on its own rails.
Supplier-invoice acceptance/refusal statebills (AP business decision)the buyer decision (accepted / refused) Green-Got makes on the received document.
B2Brouter local stateplateforme_agreee (PA adapter)the PA’s operational state of the received object.
PPF / AFNOR statusPPF concentrator (reconstructed by plateforme_agreee)the legal lifecycle status observed for the received document.

Design rule — issued and received numbering live in separate spaces. An inbound supplier Bill carries the supplier’s invoice number; a Green-Got customer’s own issued AR invoices carry Green-Got’s gap-free number (invoicing). These are independent numbering spaces — a received bill numbered INV-2026-0042 and an issued invoice numbered INV-2026-0042 are unrelated documents and never collide as a uniqueness key (AR uniqueness is per Green-Got customer; AP uniqueness is per supplier, per §4.1). When the same string appears as both an issued number and a received supplier number for one organisation, plateforme_agreee logs a conflict_detected ComplianceEventLog record and raises a non-blocking alert for operator visibility; it does not renumber either document and does not block the inbound flow — both proceed normally. Renumbering an issued invoice is forbidden (it would break gap-free numbering and immutability); renumbering a received supplier document is impossible (Green-Got does not own it).

Design rule — mark_as paid is GATED for received French invoices. Per B2Brouter’s France / DGFiP guide, the confirmed valid target states for a received French invoice via POST /invoices/{id}/mark_as are accepted and refused only. A generic mark_as paid (and the corresponding CDAR 212 / Encaissée on the received side) is NOT confirmed for the French received flow. Until B2Brouter support confirms it on staging, the platform must not assert mark_as paid for French received invoices: Green-Got’s bill payment state (axis 1) is tracked internally in bills and is not pushed to B2Brouter as paid for the French inbound path. This gate is reflected in the status support matrix at §13 and in 5. Lifecycle Statuses §14. (Buyer-side e-reporting of paid bills, where required, is a separate Flux 10 concern, not a received-invoice lifecycle push.)

5. The Canonical Structured-Invoice Handoff

The outbound handoff type from invoicing to plateforme_agreee is the EN 16931 structured data model defined in 4. Formats and Invoice Data — the seller/buyer SIREN, delivery address, goods-vs-services composition, VAT chargeability option, VAT category codes (UNCL5305), document type codes (UNCL1001), line items, and totals.

Design rule: This type is defined once, in 4. Formats and Invoice Data, and referenced everywhere else. This document does not restate the field list. invoicing populates it from its internal invoice model; plateforme_agreee maps it to B2Brouter’s JSON; B2Brouter projects it to Factur-X / UBL / CII. A field’s mandatory/optional status is governed by doc 4, not by the B2Brouter API surface or by this contract.

Invariant: Green-Got validates the canonical structured invoice against 4. Formats and Invoice Data before submission; B2Brouter’s XSD + Schematron validation is a second line of defence, not the primary contract. See 4. Formats and Invoice Data §7.

6. Eventbus Event Contract

The following events cross crate boundaries. This table is the authoritative event contract; the invoicing and bills docs must stay consistent with it.

EventDirection (producer → consumer)ProducerConsumer(s)Payload summaryPurpose
InvoiceFinalizedinvoicingplateforme_agreeeinvoicingplateforme_agreeeinvoice_id, organisation_id, structured-invoice reference (per doc 4)An immutable AR invoice is ready for regulated transmission; PA begins the outbound contract.
TransmissionStatusChangedplateforme_agreeeinvoicingplateforme_agreeeinvoicingtransmission_id, invoice_id? (set for outbound AR transmissions; NULL for inbound), legal_status (always the AFNOR XP Z12-012 layer — see doc 5), changed_at, optional reasonAn authoritative legal lifecycle status changed; AR invoice updates its projection.
InboundInvoiceReceivedplateforme_agreeebillsplateforme_agreeebillstransmission_id, organisation_id, document_ref, extracted_fields (structured supplier-invoice data), received_atA supplier e-invoice arrived and was persisted+downloaded; AP creates a Bill.
TransactionMatchedinvoicingplateforme_agreeeinvoicing (AR matching)plateforme_agreeeinvoice_id, transmission_id, matched amount (full invoice total), currency, collection dateA bank transaction settled an issued invoice in full via the payment link → drives the AR commercial lifecycle to Paid (the commercial-axis terminal; the AFNOR-legal Settled/Encaissée is a separate axis driven by MarkOutboundInvoicePaid, see §3). Does NOT by itself feed payment-data e-reporting — that is the job of PaymentCollected (below). See §7.
PaymentCollectedinvoicingplateforme_agreeeinvoicing (collection/allocation)plateforme_agreeeone AR payment-allocation ledger entry: invoice_id, transaction_id, allocated_amount, vat_breakdown[] = { vat_rate, vat_chargeability, taxable_base, vat_amount, reportability_state, submitted_to_b2brouter_at?, reported_at?, ledger_id? } (reportability is per breakdown group, keyed on {vat_rate, vat_chargeability} — there is no top-level reportability_state), collection_date, currency, source, correction_reversal_link, cumulative_collected_amount (see §11)Seller-side collection only. A real bank-level AR VAT-collection event was allocated to an issued invoice. PaymentCollected is the internal data source; the output carrier is Enriched212Men for invoiced Flux-1 operations and Flux10 only for non-invoiced operations (decision D-CARRIER), for the breakdown groups whose reportability_state is reportable. Does NOT change the AFNOR legal status by itself (see §7 and 5. Lifecycle Statuses §13). AP acquisition reporting is a separate bills-owned reportable-acquisition record, not a PaymentCollected entry.
MarkOutboundInvoicePaidcommand (within plateforme_agreee)plateforme_agreeeplateforme_agreee (PA push)transmission_id, invoice_id, collection date, applicable legal_basis (CGI art. 290 A / VAT-on-collection), allocation_correlation_id (the deterministic correlation id / allocation group id(s) of the PaymentCollected ledger write this push enriches)Outbound AR lifecycle push. Produces AFNOR status 212 (Encaissée) sent to the recipient’s PA when legally applicable (CGI art. 290 A / VAT-on-collection operations). Triggered by TransactionMatched (bank settlement) but sequenced after the ledger write + PaymentCollected so the enriched 212/MEN can be correlated to the collected payment data (see ordering invariant below). DISTINCT from PaymentCollected: it pushes a legal lifecycle status to the recipient, it does not write the payment-allocation ledger — it references that ledger via allocation_correlation_id (or PA reads the ledger by that id) to source the collection date + amount-by-VAT-rate the enriched 212/MEN carries. The same bank collection can cause both MarkOutboundInvoicePaid (212 lifecycle) and PaymentCollected (ledger write); they serve different legal projections.
ReportableAcquisitionRecordedbillsplateforme_agreeebills (AP)plateforme_agreeebill_id, organisation_id, supplier identity (name + country + VAT / local registration id), operation_type, taxable amounts, VAT treatment, invoice date, reporting period, source document ref, correction/refusal stateAP acquisition e-reporting under CGI art. 290. A received supplier invoice constitutes a reportable acquisition → feeds the Flux 10 acquisition-data overlay. This is acquisition data, NOT payment data and NOT PaymentCollected; it is buyer-side / AP-owned and carries a bill_id (never an invoice_id). See §7.

Design rule — one canonical legal status. Events carry the AFNOR XP Z12-012 legal status defined in 5. Lifecycle Statuses, not a B2Brouter API status and not an internal status. These are three separate layers, mapped by the 3-layer table in 5. Lifecycle Statuses. TransmissionStatusChanged.legal_status is always the AFNOR layer.

Design rule — settlement, collection, and legal status are different events. TransactionMatched is the full-amount settlement of the payment link: it drives the AR commercial lifecycle (→ Paid, the commercial-axis terminal) but is not the source of payment-data reporting. PaymentCollected is the VAT-collection / allocation event: it writes the payment-allocation ledger (§11) and is the internal data source whose payment data is carried on the channel-appropriate output — Enriched212Men for invoiced Flux-1 operations, the separate Flux10 payment-data flow only for non-invoiced operations (decision D-CARRIER). The existing TransactionMatched event must not be the thing that feeds payment-data reporting for service invoices; a dedicated allocation event (PaymentCollected) does. Neither event changes the AFNOR legal status of an invoice by itself — see §7.

Design rule — when ReportableAcquisitionRecorded is created. The bills-owned reportable-acquisition record is created at Bill creation, not at approval. The cross-border / reverse-charge scope that makes an acquisition reportable under CGI art. 290 is known from the supplier identity (non-FR supplier + France-located operation) and the document data, both available the moment the Bill is created — it does not depend on the buyer’s later approval/payment decision. Creating it at Bill creation guarantees the acquisition is captured even if the bill is never approved or is later disputed; approval/refusal/payment outcomes are carried as the record’s correction/refusal state and amend it (append-only), they do not gate its existence.

Invariant: Cross-crate events carry ids and references, not whole business aggregates or document bytes. Consumers re-resolve what they need from the owning crate (or download via a document_ref). This keeps crate ownership clean and avoids duplicated sources of truth.

Invariant: Every legal/status/payment/admin transition — including InvoiceFinalized, TransmissionStatusChanged, InboundInvoiceReceived, TransactionMatched, PaymentCollected, and every mark_as call — must append a record to the ComplianceEventLog (§12). The cross-crate event and its compliance-log entry share correlation_id / causation_id.

Invariant — ledger-first ordering; no bare 212/MEN push without the correlated allocation data. When one bank settlement is to produce both PaymentCollected and MarkOutboundInvoicePaid, they are sequenced deterministically:

  1. Allocation-ledger write first. invoicing appends the payment-allocation ledger entry (with its breakdown groups) in the same DB transaction as its ComplianceEventLog record.
  2. Then PaymentCollected. The ledger write emits PaymentCollected, carrying the allocation group id(s) / a deterministic correlation id.
  3. Then MarkOutboundInvoicePaid. The 212/Encaissée push is sequenced after the ledger entry exists and references it by allocation_correlation_id (or plateforme_agreee reads the ledger by that id) so the enriched 212/MEN carries the correct collection date + amount-by-VAT-rate.

A bare 212/MEN push must never be emitted before the correlated allocation data exists (it would carry no payment data, or stale/duplicated data). The same allocation_correlation_id makes the push idempotent: a retried or replayed MarkOutboundInvoicePaid for an id already pushed is a no-op (still ComplianceEventLog-recorded), so a single collection is never reported out of order or twice. This ordering is mirrored in 14. Implementation Wiring §3.

7. Payment Collection Feeds E-Reporting

When VAT is due on encaissement (collection), seller-side payment data must be e-reported (Flux 10). This section defines which event feeds that overlay and is careful to keep five distinct concepts apart (see §7.1). Buyer-side AP acquisition e-reporting (CGI art. 290) is a separate Flux 10 overlay fed by the bills-owned ReportableAcquisitionRecorded event (§6) — it is acquisition data, not PaymentCollected payment data — and is keyed on bill_id, never invoice_id.

7.1 Five distinct axes — never one enum

Payment and status involve five separate axes. They are distinct dimensions and must never be modelled as one enum:

#AxisOwner / source of truthWhat it answers
1Invoice commercial status (AR)invoicingWhere is this issued invoice in its sales/AR lifecycle (Draft → Issued → … → Paid)? (Paid is the commercial-axis terminal; the AFNOR-legal Settled/Encaissée is axis 5.)
2Outstanding balanceinvoicingHow much of the invoice total is still owed (a monetary amount, not a status)?
3VAT collection eventthe payment-allocation ledger (§11)What was actually collected at the bank, allocated to which document, with what VAT breakdown, on what date — the reportable fact for VAT-on-collection.
4B2Brouter stateplateforme_agreee (PA adapter)The PA’s operational transmission state (new/sent/accepted/…).
5AFNOR / CDAR lifecycle statusPPF concentrator (reconstructed by plateforme_agreee)The legal lifecycle status (codes 200–213) reported to / observed from the PPF.

Design rule: A payment-collection event moves axis 2 and 3 (and may move axis 1 to the AR-commercial terminal Paid when the link is paid in full). It does not by itself move axis 5 (the AFNOR legal status, where the equivalent fact is Settled/Encaissée). See §7.2 and 5. Lifecycle Statuses §13.

7.2 The collection flow

The customer-facing payment link is full-amount-only: there is no partial settlement via the link, and an invoice reaches the AR-commercial Paid status only on full collection (the settled decision; see invoicing/9). Settlement and VAT-collection reporting are nonetheless separate events:

Design rule — the collection event does not change AFNOR legal status; the explicit paid command does. PaymentCollected is the internal data source for payment-data reporting; the output carrier is Enriched212Men for invoiced Flux-1 operations and Flux10 only for non-invoiced operations (decision D-CARRIER). It does not drive the invoice’s AFNOR lifecycle status by itself. The AFNOR Encaissée (212) legal status, where it applies, is pushed by the explicit MarkOutboundInvoicePaid command (§6) — a separate lifecycle transition (axis 5), triggered by TransactionMatched, not a side effect of a single bank-level allocation. The same bank collection can produce both the 212 lifecycle push (MarkOutboundInvoicePaid) and a PaymentCollected ledger entry; they are distinct legal projections. The mapping between “fully collected” and any legal-status transition is defined in 5. Lifecycle Statuses §13; this contract only guarantees that PaymentCollected writes the ledger and feeds the channel-appropriate carrier (the enriched 212/MEN for invoiced Flux-1 operations, the separate Flux 10 payment-data flow for non-invoiced operations), and that MarkOutboundInvoicePaid carries the 212 lifecycle.

Invariant: Payment data is not e-reported when VAT is on debits, for reverse-charge operations (the buyer accounts for VAT), or when the amount is already covered by B2C transaction data. The chargeability regime carried on the canonical structured invoice (debits vs encaissement — see 4. Formats and Invoice Data §4) governs whether a PaymentCollected entry results in Flux 10 payment-data reporting; the per-breakdown-group reportability_state (§11) records the outcome.

Design rule: B2Brouter performs no payment processing. Collection happens on Green-Got’s own rails (payment links, bank transactions). TransactionMatched and PaymentCollected are both Green-Got-internal. B2Brouter / the recipient’s PA learn of collection through two distinct plateforme_agreee pushes, each from the same bank settlement: the AFNOR 212 (Encaissée) lifecycle status via MarkOutboundInvoicePaid (where legally applicable — CGI art. 290 A / VAT-on-collection) and the Flux 10 payment-data e-reporting overlay fed by PaymentCollected. These are separate legal projections, not a single overlay. See B2Brouter — Payment.

8. The Organisation-Level Link

The current B2Brouter account id (b2brouter_account_id) lives on the organisation / enrollment (one per SIREN). It is the single source of truth for which account Green-Got uses today. See 9. Mandate and Onboarding §7.

Design rule — current account is org-level; the transmission keeps an immutable snapshot. There are two distinct facts, and they live in two places:

Design rule — why the snapshot is stored per transmission. The snapshot is required for audit (proving which provider account a legal exchange went through, years later), replay (re-deriving status from the exact account context), and provider migration (after the org moves to a new account or a new PA, historical transmissions must still point at the account that carried them). Resolving the current org account would silently rewrite history after a migration, breaking the legal trail. The snapshot is therefore captured once and never updated.

Invariant: A transmission references (a) its owning business record (the AR invoice_id for outbound, or the AP Bill for inbound — see §8.1), (b) the organisation/enrollment (to resolve the current account), and (c) the immutable b2brouter_account_id snapshot of the account that carried it. (a) and (c) are historical facts; the current account is read from the enrollment, never from the transmission.

8.1 PaTransmission data contract

A PaTransmission is the transport-layer record for one regulated exchange. It is direction-aware and must not be modelled with a single non-null invoice_id that FKs to invoicing for both directions — that would violate the AR/AP boundary (§4). The canonical field set:

FieldType / targetNotes
idPK (pat_)transmission id.
directionenum outbound | inboundthe axis that decides which document reference is set.
invoice_id?nullable FK → invoicing.invoiceset only when direction = outbound; the issued AR invoice. NULL for inbound.
bill_id?nullable FK → bills.billthe inbound AP document/bill created from this transmission; set only when direction = inbound. NULL for outbound.
organisation_idFK → Organisation / enrollmentresolves the current B2Brouter account; the enrollment link.
b2brouter_account_idimmutable snapshot (not a live FK)the account that carried this exchange, captured at call time (per §8).
b2brouter_invoice_idprovider document idB2Brouter’s id for the invoice object on this exchange.
b2brouter_document_idsprovider ids (CDAR / attachments)other provider-side ids needed to fetch documents/receipts.
legal_statusAFNOR code (200–213)authoritative legal lifecycle status, reconstructed from raw status + CDAR.
raw_status_payload / raw_payload_hashraw PA payload + hashthe raw B2Brouter API status + CDAR; the hash makes tampering detectable and supports the ComplianceEventLog (§12).
idempotency_keystringdedupe key (B2Brouter invoice id for inbound webhooks; submission key for outbound).
request_id / retry_attemptstring / intprovider request id and retry counter for replay and reconciliation.

Design rule — raw provider payloads follow field-level retention. raw_status_payload / raw_payload_hash and any other raw provider payload persisted on the transmission are governed by the field-level retention rules in 13. Privacy and Data Protection: raw evidence is kept hash-only for the legal/audit trail, the full raw payload (where retained at all for support) lives in a short-TTL encrypted vault, and secrets / signed download URLs are never persisted. See 13. Privacy and Data Protection for the per-category split.

Invariant — exactly one document reference is set. direction = outbound ⇒ invoice_id IS NOT NULL AND bill_id IS NULL; direction = inbound ⇒ bill_id IS NOT NULL AND invoice_id IS NULL. No transmission ever references both an AR invoice and an AP bill. This is the data-layer enforcement of the AR/AP boundary. The ER schema and FK table are in 12. Data Model §3–§4.

11. The Payment-Allocation / VAT-Collection Ledger (Canonical)

This section is THE authoritative definition of the payment-allocation ledger. Other docs reference it here.

The customer-facing payment link is full-amount-only (no partial settlement via the link), but the platform must model real bank-level collection so that VAT-on-collection payment data can be reported accurately (Flux 10). The bridge between “money actually arrived at the bank” and “what we report to DGFiP” is the payment-allocation / VAT-collection ledger: an append-only ledger of collection events, each allocating a real bank movement (or correction/reversal) to a document with a VAT breakdown.

It is distinct from the AR commercial status, from the outstanding balance, from the B2Brouter state, and from the AFNOR legal status (the five axes of §7.1). It is not a status enum.

11.1 Ledger entry fields

FieldTypeMeaning
invoice_idreferencethe AR invoicing.invoice (money in) the collection is allocated to. The ledger and PaymentCollected are seller-side (AR) collection only; buyer-side AP acquisition reporting is a separate bills-owned reportable-acquisition record, not a payment-allocation entry.
transaction_idFK → core_banking BankTransactionthe real bank movement this entry allocates.
allocated_amountmoneythe portion of the transaction allocated to this document (supports a transaction split across several documents, and a document collected across several transactions).
vat_breakdownlist of breakdown groups, each { vat_rate, vat_chargeability, taxable_base, vat_amount, reportability_state, submitted_to_b2brouter_at?, reported_at?, ledger_id? }the VAT decomposition of allocated_amount, grouped by both the rate and the chargeability regime (débits vs encaissement) — the reportable figures for VAT-on-collection. All reporting fields (reportability_state, submitted_to_b2brouter_at, reported_at, ledger_id) live inside each group, never at the top level — they advance per group. See the per-group reporting note below and the worked mixed-invoice example.
collection_datedatethe value/collection date of the bank movement; the date the VAT becomes due on collection.
currencyISO 4217currency of the collection (reported when not EUR).
sourceactor/systemwho/what produced the entry (payment-link settlement, manual allocation by a member, automated matcher, reversal job).
correction_reversal_linknullable reference → ledger entryfor a correction or reversal, points at the entry it amends/reverses (append-only correction semantics — see §11.2).
cumulative_collected_amountmoneyrunning total collected against the document after this entry (drives the outstanding-balance axis).

The per-group reporting fields (submitted_to_b2brouter_at, reported_at, ledger_id) live inside each vat_breakdown group — there is NO top-level reporting field on the entry. Each group carries:

Per-group field (in vat_breakdown[])TypeMeaning
submitted_to_b2brouter_atnullable timestampwhen this group’s reportable VAT was submitted to B2Brouter for the payment-data overlay (NULL until submitted). The timestamp the §4.3 deadline-breach rule compares against.
reported_at (a.k.a. reported_to_dgfip_at)nullable timestampwhen DGFiP registered the payment-data report for this group (NULL until registered). Set when the group transitions to reported (see the binding below).
ledger_idnullable back-reference → tax-report ledgerthe B2Brouter/DGFiP per-calendar-day ledger this group landed in, for joining the group to its tax-report acknowledgement (deadline-breach reconciliation, §4.3).

Because a single entry can carry several breakdown groups with independent reportability, these timestamps and the ledger back-reference cannot be a single per-entry value; they advance per group. (This matches the per-group nesting in 12. Data Model.)

Design rule — reportability_state is per breakdown group, not per entry. Each vat_breakdown group carries its own reportability_state, one of reportable (VAT on collection), not_reportable (VAT on debits / reverse-charge / already in B2C data), reported, or superseded (by a correction). It is not a single per-entry field: a single bank collection that pays a mixed invoice (some lines on débits, some on encaissement) produces multiple breakdown groups keyed on {vat_rate, vat_chargeability}, each with an independent state. This prevents a service-line collection at 20 % from dragging a goods-line at the same 20 % into the payment-data report (goods VAT is chargeable at issuance under CGI art. 269, not at collection). plateforme_agreee reports only the groups whose state is reportable, over the channel-specific carrier (D-CARRIER: Enriched212Men for invoiced Flux-1 operations, Flux10 only for non-invoiced). submitted_to_b2brouter_at / reported_at / ledger_id advance per group as each reportable group is submitted/registered.

Design rule — what binds a group to reported. A breakdown group transitions reportable → reported only when its payment-data submission is confirmed registered by the DGFiP-enabled B2Brouter tax-report ledger (the registered tax-report lifecycle state, i.e. accepted into the DGFiP concentrator), not on the earlier acknowledged state (B2Brouter received the submission but DGFiP has not yet registered it). The submission rides the channel-specific carrier (D-CARRIER: the enriched 212/MEN for an invoiced Flux-1 op — itself staging-gated, see L-4 — or a Flux10 ledger submission for a non-invoiced op); either way reported binds to DGFiP registered, not to the bare paid transition. acknowledged is an in-flight receipt; registered is the legally-effective report. On the registered signal, plateforme_agreee sets the group’s reported_at (reported_to_dgfip_at) and ledger_id. This is the same Flux 10 / tax-report lifecycle the §4.3 deadline-breach rule reconciles against (see 7. E-Reporting).

11.1.1 Worked example — mixed-invoice collection

An issued invoice has two lines, both at 20 %: goods €1,000 HT (VAT €200, chargeability débits) and services €500 HT (VAT €100, chargeability encaissement). The customer pays the full €1,800 TTC in one bank transfer. One PaymentCollected ledger entry is appended with allocated_amount = €1,800 and a two-group vat_breakdown:

Groupvat_ratevat_chargeabilitytaxable_basevat_amountreportability_state
Goods20 %débits€1,000€200not_reportable
Services20 %encaissement€500€100reportable

plateforme_agreee reports only the services group (€500 base / €100 VAT) over the channel-specific payment-data carrier (Enriched212Men for this invoiced Flux-1 invoice; Flux10 only applies to non-invoiced ops); the goods group is excluded (its VAT was already chargeable at issuance). With the old single-reportability_state shape both lines collapsed into one {20 %, …} group and the goods VAT would have been over-reported. The two groups advance their submitted_to_b2brouter_at / reported_at / ledger_id independently — only the services group ever gets non-NULL payment-data timestamps.

Invariant — append-only. Ledger entries are never updated or deleted. A mistake is corrected by appending a reversing entry that links back via correction_reversal_link; the original stays for audit. cumulative_collected_amount is recomputed from the entry chain, never mutated in place.

Invariant — AR-only document reference. Each entry references an invoice_id (AR invoicing.invoice). The payment-allocation ledger is seller-side collection only; there is no bill_id on a ledger entry. Buyer-side (AP) acquisition reporting, where it applies, is a separate bills-owned reportable-acquisition record — not a payment-allocation entry and not PaymentCollected payment data.

Invariant — idempotent allocation (dedup key). Each ledger entry carries a deterministic dedup key (invoice_id, transaction_id, allocated_amount, collection_date, source) (a correction/reversal entry additionally keys on its correction_reversal_link group so it is not collapsed with the entry it amends). Re-delivery of the same PaymentCollected trigger (retried matcher, replayed event, double webhook) must not append a second entry for the same key — the write is upserted on the key, and a duplicate is a no-op that still appends a ComplianceEventLog record noting the deduplicated attempt. This is the ledger’s defence against double-counting a single bank collection.

Invariant — no over-allocation. The sum of allocated_amount across all non-superseded entries for one invoice_id (after applying correction/reversal links) must never exceed the invoice total TTC. An allocation that would push cumulative_collected_amount above the invoice total is rejected (it indicates a mis-match), not silently capped. Reversals reduce the cumulative total so a later correct re-allocation stays within bounds.

11.2 Correction and reversal semantics

Real-world collection is messy: a matched transaction is later found to belong to a different invoice, a payment is reversed (chargeback / returned transfer), or a figure changes after it was reported to DGFiP. The ledger handles all three by appending:

Design rule: Each ledger write emits PaymentCollected (§6) and appends a ComplianceEventLog record. plateforme_agreee consumes PaymentCollected and reports the reportable breakdown groups over the channel-specific carrier (D-CARRIER: Enriched212Men for invoiced Flux-1 ops, Flux10 for non-invoiced); it does not change the invoice’s AFNOR legal status from the ledger alone (§7.2). The ledger entity appears in the ER schema and FK table at 12. Data Model §3–§4.

12. The ComplianceEventLog / Outbox (Canonical)

This section is THE authoritative definition of the ComplianceEventLog. Other docs reference it here.

Cross-crate events (§6) carry business payloads but not the compliance-grade metadata a French e-invoicing audit needs (who, when, in what causal chain, against which provider request, idempotently). The ComplianceEventLog is an append-only, immutable outbox that records every legal/status/payment/admin transition with that metadata. It doubles as the transactional outbox: a transition and its log entry are written in the same DB transaction, and the cross-crate event / B2Brouter call is dispatched from the log.

12.1 Schema

FieldTypeMeaning
event_idPK (uuid)unique id of this log record.
schema_versionint/semverversion of the event payload schema, so replay survives schema evolution.
actoractor refthe member, system job, or external system that caused the transition.
causation_idevent refthe event/command that directly caused this one.
correlation_ididgroups every record in one end-to-end flow (e.g. one invoice’s whole lifecycle).
source_systemenuminvoicing | bills | plateforme_agreee | b2brouter | ppf | business_api.
event_typeenumthe transition (InvoiceFinalized, TransmissionStatusChanged, PaymentCollected, MarkAsAccepted, AuditLogCorrection, conflict_detected, an admin action, …).
raw_payload_hashhashhash of the raw payload (B2Brouter status + CDAR, request/response body), making tampering detectable. Per the field-level retention rule (13. Privacy §4.1) the hash is the permanent (category 2) audit field; the raw bytes are never permanent — where retained at all (support/dispute), they live only in the short-TTL encrypted, access-controlled vault (category 3), and any signed URL/secret inside them is never persisted (category 4).
b2b_request_idstringthe B2Brouter request id this transition corresponds to (for replay/reconciliation).
retry_attemptintretry counter for the dispatch.
occurred_attimestampwhen the transition happened.
deprecated_by_event_idnullable event ref → ComplianceEventLogwhen this record was a spurious / superseded entry, points at the later AuditLogCorrection record that deprecates it. NULL for a live record. The original is never mutated or deleted — this field is set on it by appending the correction, then back-stamping the pointer (the only permitted write, and itself logged).

Invariant — append-only / immutable. ComplianceEventLog records are never updated or deleted; they are retained for the legal retention horizon (permanent legal-artifact retention). Corrections are new records linked by correlation_id / causation_id.

Design rule — soft-deprecation of spurious entries (AuditLogCorrection). A record written in error (a spurious transition, a mis-fired event, a duplicate later found redundant) is never removed. Instead a new ComplianceEventLog record of event_type = AuditLogCorrection is appended, sharing the original’s correlation_id and pointing at it via causation_id; the original’s deprecated_by_event_id is set to the correction’s event_id. Readers treat a record with a non-NULL deprecated_by_event_id as soft-deprecated (excluded from the live projection, retained for audit). This preserves the immutable, append-only trail required by French e-invoicing audit while letting the operational read-model ignore mistakes. It mirrors the ledger’s append-only correction semantics (§11.2).

12.2 What must be logged

Invariant: Every legal, status, payment, or admin transition writes a ComplianceEventLog record in the same transaction as the state change — including InvoiceFinalized, every TransmissionStatusChanged, InboundInvoiceReceived, TransactionMatched, every PaymentCollected ledger write, every B2Brouter mark_as call, and administrative overrides. The cross-crate event (§6) and its log record share correlation_id / causation_id.

Design rule — correction-event semantics. Unmatched/rematched payments, reversals, and post-report changes (§11.2) each append a correction ComplianceEventLog record that links (via causation_id) to the record it amends and captures the before/after. Nothing is rewritten; the audit trail is the full append-only chain. The ComplianceEventLog entity appears in the ER schema and FK table at 12. Data Model §3–§4.

13. Status Support Matrix

Status lives in different layers, each authoritative for a different fact (§7.1, and 5. Lifecycle Statuses §1). The source of truth is defined by fact:

The matrix below classifies the status-bearing facts by how Green-Got handles each:

Status / factPersisted-onlyObserved-from-B2BrouterEmitted-to-PPF/DGFiPUnsupported / gatedUI-only
AR commercial lifecycle (invoicing)
Outstanding balance (invoicing)
Payment-allocation ledger entry (§11)✅ (per reportable group; carrier = enriched 212/MEN for invoiced Flux-1 ops, Flux 10 payment data only for non-invoiced ops — D-CARRIER)
AFNOR mandatory outbound (200/210/212/213)✅ (reconstructed)
AFNOR recommended (201–209, 211)✅ (via CDAR)— (inter-platform, not reported up)
B2Brouter API status (outbound)✅ (raw)
Inbound B2Brouter local state✅ (raw)✅ (polling-authoritative)
Inbound accepted / refused (received French)✅ (mark_as to supplier)
Inbound paid / CDAR 212 on received French⚠️ gated (B2Brouter France support unconfirmed — §4)
Green-Got bill payment state (bills)
Tax-report (Flux 10) lifecycle
business_api Draft|Pending|Settled|Canceled✅ (projection only)

Design rule — reconciling “projection” vs “single source of truth”. These are not in conflict once split by fact: the AR commercial lifecycle in invoicing is the single source of truth for the commercial state (axis 1), while the AFNOR legal status is a projection in invoicing (the source of truth being B2Brouter/PPF, axis 5). A doc that says “GG status is a projection” is talking about the legal status; a doc that says “the internal model is the single source of truth” is talking about the canonical internal model that invoicing/bills own for their own commercial/processing lifecycle. Both are true on their own axis. See 5. Lifecycle Statuses §1 and §9.1 there.

9. Related Documents

10. Sources

11. European Extension

European Extension — Shared Core + Country Packs

This document sets the target architecture for taking Green-Got e-invoicing beyond France to the EU: a country-agnostic shared core plus per-country packs. It is forward-looking — the current implementation is France-first — but the seams are defined now so adding a country never touches core logic. It builds directly on the canonical-model-plus-mappers design in 5. Lifecycle Statuses §1.1.

1. Terminology

Shared reform terms are in the glossary. Terms specific to this doc:

2. The Principle — EN 16931 Is the Stable Core, the Rest Is a Pack

Design rule — build the domain on EN 16931, vary everything else in packs. EN 16931 is legally mandated across the EU and is the only guarantee of cross-border interoperability (ViDA makes it the cross-border standard from 2030). Green-Got’s canonical internal model is an EN 16931 superset (4. Formats and Invoice Data). Everything a specific country adds — its clearance model, format, portal, identifier scheme, lifecycle status codes, archiving rules — is reached through a mapper / adapter, exactly as the FR transport is today (§1.1). Adding a country is a new pack, not a change to the core.

This is the same pattern already in place for France, generalized:

Today (France)Generalized (EU)
Canonical internal model (EN 16931 superset)unchanged — the shared core
*_to/from_b2brouter_data mappersone transport adapter per PA / per country CTC
AFNOR XP Z12-012 status mapperone lifecycle mapper per country
Annuaire routingone directory/identifier resolver per country
Factur-X / UBL / CII generation (by the PA)one format set per country (the PA or our serializer)

3. What Is Shared vs What Varies

Shared core (country-agnostic):

Country pack (per country):

3.1 The variation surface (representative countries)

DimensionFranceItalySpainGermanyPoland
ModelPost-audit, 5-corner (PPF/PA)Clearance (SDI)Hybrid (FACe B2G; SII/VeriFactu B2B)Post-audit, peer-to-peerClearance (KSeF)
Legal issuanceon exchangeon SDI acceptanceon issueon issueon KSeF acceptance
Format(s)Factur-X, UBL, CIIFatturaPA (UBL CIUS)Facturae (B2G), UBL (B2B), VeriFactu hashXRechnung, ZUGFeRDKSeF XML (FA)
TransportPA network (B2Brouter) + AnnuaireSDI hubFACe / peer + AEAT reportingPeppol / peerKSeF hub
IdentifierSIREN/SIRET + AnnuaireCodice Destinatario / PECNIF + admin codesUSt-IdNr (VAT)NIP / REGON
LifecycleAFNOR 14 codes (CDAR)implicit (SDI receipts)implicit (FACe/SII)none mandatedimplicit (KSeF)
Archiving6 yr (10 yr accounting), PAF10 yr (SDI copy)4 yr (+ SII/VeriFactu)10 yr (GoBD)5 yr (KSeF copy)

Sources in §6. Key insight: format, clearance model, and transport are orthogonal — they vary independently and all sit on the same EN 16931 core.

3.2 VAT normalization — country packs map local codes onto canonical fields

VAT category and exemption coding is not uniform across the EU even though every country builds on EN 16931. EN 16931 itself standardizes the carriers — the VAT category code (BT-118, drawn from the UN/ECE UNCL5305 code list: S standard, Z zero, E exempt, AE reverse charge, K intra-EU, G export, O out of scope), the VAT rate (BT-119), and the exemption-reason carriers (BT-120 free text and/or BT-121 VATEX code). But each country/CIUS prescribes which codes are valid, which exemption reasons it recognises, and which national conventions or extension code lists it layers on top (FR via AFNOR XP Z12-012; IT FatturaPA Natura codes; ES; etc.). A country’s local VAT vocabulary is therefore a pack concern, not a core concern.

Design rule — packs normalize local VAT codes onto the canonical fields; the domain reads canonical only. The shared core’s canonical EN 16931 model carries VAT exclusively as the canonical fields — VAT category (UNCL5305), VAT rate, and the BT-120/BT-121 exemption-reason carriers — together with the orthogonal vat_chargeability dimension already modeled for France (see the per-{vat_rate, vat_chargeability} breakdown in 10. Integration Contracts §11 and 7. E-Reporting). Each country pack owns a VAT-code mapper that:

This is the same mapper seam as the lifecycle/transport/directory adapters: the invoicing / bills domain never sees a local VAT code and never branches on country — it reads and writes only the canonical VAT fields. Adding a country’s VAT specifics is a new mapper in that country’s pack, not a change to the canonical model.

4. Proposed Crate Structure

e_invoicing_core/      # shared, country-agnostic
  - EN 16931 canonical model (superset) + calc/validation base
  - UBL + CII parse/serialize
  - traits: TransportAdapter, LifecycleMapper, DirectoryResolver, FormatSerializer, VatCodeMapper, Archiver
  - canonical internal status + error model

peppol/                # shared transport (AS4 + SML/SMP); default for post-audit countries
  - implements TransportAdapter

country packs (one per country, behind a feature/registration):
  fr/   # France — TODAY this is the `plateforme_agreee` crate
        # PA transport via B2Brouter; Annuaire resolver; AFNOR XP Z12-012 lifecycle + CDAR; Factur-X
  it/   # Italy — SDI clearance connector; FatturaPA; Codice Destinatario; clearance orchestration
  es/   # Spain — FACe / SII / VeriFactu; Facturae; hash-chain integrity
  de/   # Germany — Peppol-native; XRechnung/ZUGFeRD; GoBD archiving
  pl/   # Poland — KSeF clearance connector; KSeF XML; clearance orchestration

Design rule — packs implement traits; the core never branches on country. A country pack provides a TransportAdapter (send/receive/status), a LifecycleMapper (country status set ↔ canonical internal status), a DirectoryResolver (identifier → routing), a FormatSerializer (canonical model ↔ country format), a VatCodeMapper (local VAT/exemption coding ↔ canonical UNCL5305 + BT-120/BT-121 carriers — see §3.2), and an Archiver. Core/domain code (invoicing, bills) is selected the country pack at the edge and otherwise speaks only the canonical model. There is no match country { … } in the domain.

Design rule — clearance is a transport-adapter concern, not a domain concern. A clearance pack (IT/PL) makes legal issuance wait for the hub’s acceptance inside its TransportAdapter/orchestration; a post-audit pack (FR/DE) issues then reports. The invoicing domain emits “finalize → transmit” identically; the pack decides whether “issued” means “accepted by the hub”. The canonical internal status already distinguishes Submitted from Accepted, which expresses both models.

Design rule — B2Brouter is one transport, not the architecture. B2Brouter is a Peppol Access Point + FR PA and already covers many post-audit countries via Peppol; it implements TransportAdapter for those. It cannot be the transport for clearance hubs (SDI, KSeF) — those need direct connectors in their packs. So b2brouter is an adapter the fr (and possibly de, Peppol) pack uses, not the core.

5. How the Current Code Maps Onto This

Design rule — anticipate, don’t pre-build (YAGNI with seams). Do not build it/es/de/pl packs before they are needed. Build France well, keep the trait seams clean (transport, lifecycle, directory, format, archiver), and keep all France specifics behind those seams so the first extraction is cheap. The cost of multi-country is paid when the second country ships, not before — but the seams cost nothing to maintain now.

6. Sources

12. Data Model

Data Model — Entities and Relationships

This is the consolidated entity-relationship model across the e-invoicing crates (organisation, invoicing, bills, plateforme_agreee) and core_banking. The individual entities are defined in their own docs (invoicing/3, bills/2, 9. Mandate and Onboarding, 10. Integration Contracts); this doc shows how they fit together.

1. The three words people confuse

TermWhat it isCrate
OrganisationThe Green Got business customer — the company that uses the app. The system-of-record identity; everything hangs off it. A natural person (a member) acts on its behalf.organisation
ClientA customer of the organisation — the party the organisation issues an invoice to (AR).invoicing
SupplierA vendor of the organisation — the party the organisation receives a bill from (AP).bills

So: Organisation → issues Invoices to its Clients (money in); Organisation → receives Bills from its Suppliers (money out). “Customer” in the product sense = Organisation; the organisation’s own customers = Clients.

2. Where B2Brouter / the PA sit

3. The complete schema

erDiagram
    ORGANISATION ||--|| B2BROUTER_ACCOUNT : "one per SIREN"
    ORGANISATION ||--|| ENROLLMENT : "enrolled as (1/SIREN)"
    ORGANISATION ||--o{ MANDATE_CONSENT : "grants (active + history)"
    ORGANISATION ||--o{ DESIGNATION_LOSS_ALERT : "alerted on inbound-designation loss"
    ORGANISATION ||--o{ CLIENT : "its customers (AR)"
    ORGANISATION ||--o{ SUPPLIER : "its vendors (AP)"
    ORGANISATION ||--o{ INVOICE : "issues"
    ORGANISATION ||--o{ BILL : "receives"
    ORGANISATION ||--o{ BANK_TRANSACTION : "account activity"

    CLIENT ||--o{ CONTACT : "has"
    CLIENT ||--o{ QUOTE : "quoted"
    CLIENT ||--o{ INVOICE : "billed to"

    QUOTE ||--o| SIGNATURE : "signed via"
    QUOTE ||--o| INVOICE : "converts to"

    INVOICE ||--o{ INVOICE_LINE : "has"
    INVOICE ||--o{ CREDIT_NOTE : "corrected by Avoir (avo_, 381)"
    INVOICE ||--o| PA_TRANSMISSION : "transmitted by (outbound)"
    INVOICE ||--o{ PAYMENT_LINK : "paid via"
    INVOICE ||--o{ INVOICE_TX_MATCH : "settled by"
    INVOICE ||--o{ PAYMENT_ALLOCATION : "VAT-collected (AR)"
    BANK_TRANSACTION ||--o{ INVOICE_TX_MATCH : "incoming credit"

    SUPPLIER ||--o{ BILL : "issues to us"
    BILL ||--o{ BILL_LINE : "has"
    BILL ||--o| PA_TRANSMISSION : "created from (inbound, PA channel)"
    BILL ||--o{ BILL_PAYMENT : "paid by"
    BILL ||--o{ REPORTABLE_ACQUISITION : "e-reported acquisition (AP, art. 290)"
    BANK_TRANSACTION ||--o{ BILL_PAYMENT : "outgoing debit"
    BANK_TRANSACTION ||--o{ PAYMENT_ALLOCATION : "allocated movement"

    ORGANISATION ||--o{ PA_TRANSMISSION : "transmits (resolves current account)"
    B2BROUTER_ACCOUNT ||..o{ PA_TRANSMISSION : "snapshot of carrying account (immutable)"

    PAYMENT_ALLOCATION ||--o| PAYMENT_ALLOCATION : "corrects/reverses"

    ORGANISATION { string organisation_id PK }
    B2BROUTER_ACCOUNT {
        string b2brouter_account_id PK "external, 1/SIREN"
        string organisation_id FK "→ Organisation"
        string status "active | suspended | closed (provider-side, as last observed)"
        bool dgfip_enabled "tax_report_settings/dgfip enabled (Annuaire-registered)"
        string routing_scope "0225:SIREN (MVP); SIRET/internal DISABLED (P-2)"
        timestamp created_at "when the account was created in B2Brouter"
    }
    ENROLLMENT {
        string organisation_id PK "1 per French legal entity / SIREN"
        string status "MandateCaptured|AccountCreated|DgfipEnabled|RoutingDeclared|Enrolled | KybBlocked|AccountFailed|DgfipEnablementFailed"
        string b2brouter_account_id "current account (live link)"
        bool dgfip_enabled
        string routing_scope "0225:SIREN at MVP"
        int retry_attempt "per-step retry counter"
        int max_retry "configured ceiling"
        timestamp inbound_designation_lost_at "null while still designated inbound PA"
    }
    MANDATE_CONSENT {
        string id PK "org-level"
        string organisation_id FK "→ Organisation"
        json scope_flags "canonical authorization set {issue, receive, e_report, payment_data, archive, support} — all true at enrollment; the single per-capability gate (see note below)"
        timestamp accepted_at
        string consent_version "exact contract text/version (pa-mandate-v1)"
        string accepted_by "authenticated user id"
        string signatory_role "claimed authority to bind the company"
        string kyb_reference_id "KYB record/version backing authority at capture"
        timestamp kyb_verified_at
        string status "active | revoked | superseded"
    }
    DESIGNATION_LOSS_ALERT {
        string id PK "dla_"
        string organisation_id FK "→ Organisation / enrollment"
        timestamp detected_at "= inbound_designation_lost_at"
        timestamp first_alerted_at
        json resend_history "[{sent_at, channel}]"
        timestamp acknowledged_at
        string acknowledged_by
        string outcome "intended_switch | redesignate_requested"
    }
    CLIENT { string id PK "cli_" }
    CONTACT { string id PK }
    SUPPLIER { string id PK }
    QUOTE { string id PK "quo_" }
    SIGNATURE { string id PK "done elsewhere" }
    INVOICE { string id PK "inv_" }
    CREDIT_NOTE { string id PK "avo_ — the Avoir (credit note, AFNOR type 381)" }
    INVOICE_LINE { string id PK }
    BILL { string id PK "bil_" }
    BILL_LINE { string id PK }
    PA_TRANSMISSION {
        string id PK "pat_"
        string direction "outbound | inbound"
        string invoice_id FK "→ invoicing.invoice, only when outbound"
        string bill_id FK "→ bills.bill, only when inbound"
        string organisation_id FK "→ org/enrollment (current account)"
        string b2brouter_account_id "IMMUTABLE snapshot of carrying account"
        string b2brouter_invoice_id "provider document id"
        string b2brouter_document_ids "CDAR / attachment ids"
        string legal_status "AFNOR code 200-213"
        string raw_payload_hash "tamper-evident hash of raw PA payload"
        string legal_artifact_ref "stored legal artefact (core S3) — the compliance copy"
        string content_hash "hash of the stored legal artefact at retrieval"
        string provider_invoice_id "B2Brouter provider invoice id"
        timestamp retrieval_timestamp "when the legal artefact was fetched + hashed"
        string legal_download_path "B2Brouter download_legal_url / as/legal"
        string validation_version "validation ruleset in effect at transmission"
        string rendering_version "renderer version in effect at transmission"
        json audit_metadata "provider/audit metadata (request id, status history, validation output)"
        string idempotency_key
        string request_id
        int retry_attempt
    }
    PAYMENT_ALLOCATION {
        string id PK "pal_, append-only"
        string invoice_id FK "→ invoicing.invoice (AR) — seller-side only"
        string transaction_id FK "→ core_banking BankTransaction"
        money allocated_amount
        json vat_breakdown "[{vat_rate, vat_chargeability, taxable_base, vat_amount, reportability_state, submitted_to_b2brouter_at?, reported_at?, ledger_id?}] — reportability is PER GROUP (keyed on rate+chargeability); NO top-level reportability_state"
        date collection_date
        string currency "ISO 4217"
        string source "link | manual | matcher | reversal"
        string correction_reversal_link FK "→ PaymentAllocation (self)"
        money cumulative_collected_amount
    }
    REPORTABLE_ACQUISITION {
        string id PK "rac_, bills-owned (AP)"
        string bill_id FK "→ bills.bill (AP) — buyer-side only, never invoice_id"
        string organisation_id FK "→ org/enrollment"
        string supplier_name "supplier identity"
        string supplier_country "ISO country"
        string supplier_tax_id "VAT / local registration id"
        string operation_type "acquisition operation type"
        json taxable_amounts "taxable base(s) by rate"
        string vat_treatment "reverse-charge / domestic / etc."
        date invoice_date
        string reporting_period "Flux 10 period"
        string source_document_ref "received original ref"
        string correction_refusal_state "correction / refusal status"
    }
    COMPLIANCE_EVENT_LOG {
        string event_id PK "uuid, append-only"
        int schema_version
        string actor
        string causation_id
        string correlation_id
        string source_system "invoicing|bills|plateforme_agreee|b2brouter|ppf|business_api"
        string raw_payload_hash
        string b2b_request_id
        int retry_attempt
    }
    BANK_TRANSACTION { string id PK "core_banking, direction" }
    INVOICE_TX_MATCH { string invoice_id FK }
    BILL_PAYMENT { string bill_id FK }
    PAYMENT_LINK { string id PK }

Note on the ledger / log relationships. PAYMENT_ALLOCATION is seller-side (AR) collection only: it references an invoice_id (AR invoicing.invoice) and the BankTransaction movement it allocates; corrections/reversals self-link via correction_reversal_link. There is no bill_id on a ledger entry — buyer-side AP acquisition reporting is the separate bills-owned REPORTABLE_ACQUISITION record (see 10. Integration Contracts §11). REPORTABLE_ACQUISITION is the AP (buyer-side) record feeding Flux 10 acquisition data (CGI art. 290): it references a bill_id (never an invoice_id), holds supplier identity and the acquisition’s tax facts, and is the source of the ReportableAcquisitionRecorded event (10. Integration Contracts §6). It is acquisition data, not payment data — never a PaymentAllocation / PaymentCollected entry. COMPLIANCE_EVENT_LOG is the append-only outbox/audit log keyed by correlation_id / causation_id across crates; it is not FK’d into the business graph (it records every transition, including B2Brouter mark_as calls), so it is shown standalone.

Note on MANDATE_CONSENT.scope_flags — the canonical authorization set. The mandate carries a single scope_flags set, not a separate scope enum: the canonical flags are issue, receive, e_report, payment_data, archive, support (defined on MandateEvidence in 9. Mandate and Onboarding §4.5). There is no manage_status flag — status management (mark_as accepted/refused/paid) is not a separate legal consent; it is part of the issue (outbound) / receive (inbound) authorization, never its own scope. Each regulated action is gated on exactly one authoritative flag:

Regulated actionAuthoritative flag
Issue (Flux 1 outbound send, incl. the outbound mark_as lifecycle pushes)issue
Receive (inbound reception + the buyer-side mark_as accepted/refused)receive
E-reporting transaction data (B2C / cross-border sales and AP ReportableAcquisitionRecorded acquisition data)e_report
Payment data + MEN (seller-side VAT-on-collection payment data — enriched 212/MEN for invoiced ops, Flux 10 payment data for non-invoiced ops)payment_data
Archive access + export (legal-artefact access and PA-switch portability export)archive
Support accesssupport

e_report (transaction/acquisition data) and payment_data (seller-side payment data) are separate flags and are never merged. The gating rules are authoritative in 9. Mandate and Onboarding §4.5.

4. Key foreign keys (who points at whom)

FromFKToNotes
B2BrouterAccountorganisation_idOrganisationone per SIREN; lightweight wrapper (status, dgfip_enabled, routing_scope, created_at), not a provider-metadata cache
Enrollmentorganisation_id (PK)Organisationone per SIREN; onboarding state machine + failure states + retry_attempt/max_retry + inbound_designation_lost_at
DesignationLossAlertorganisation_idOrganisation / enrollmentresend-until-ack alert on inbound-designation loss
MandateConsentorganisation_idOrganisationorg-level, status: active → revoked / superseded (revoked = withdrawn with no successor; superseded = replaced by a new active record — see 9. Mandate §4.1)
Client / Supplier / Invoice / Billorganisation_idOrganisationevery business record is org-scoped
Invoiceclient_idClientthe buyer
Invoicequote_id?Quoteif converted from a quote
Avoir (avo_)corrects_invoice_idInvoicea 381 against an invoice; never an edit. Avoir is the canonical entity name (id prefix avo_); CREDIT_NOTE in the ER diagram above is the same entity (the invoicing docs use Avoir/avo_ per decision D8 — they are equivalent, not two models).
Billsupplier_idSupplierthe vendor
Billtransmission_id?PaTransmissioninbound transmission; null for email/upload channels
PaTransmissioninvoice_id?Invoicenullable; set only when direction = outbound (AR). NULL for inbound.
PaTransmissionbill_id?Billnullable; set only when direction = inbound (AP). NULL for outbound.
PaTransmissionorganisation_idOrganisation / enrollmentresolves the current B2Brouter account at call time.
PaTransmissionb2brouter_account_id(snapshot, not a live FK)immutable historical snapshot of the account that carried this exchange — for audit / replay / provider migration. Never follows the org’s current account.
PaTransmissionlegal_artifact_ref(core S3 object ref)the stored transmitted/PA-generated legal artefact — the compliance copy (outbound). Regeneration is display-only.
PaTransmissioncontent_hash(field)hash of the stored legal artefact, recorded at retrieval_timestamp; proves the copy is intact and matches the provider record.
PaTransmissionprovider_invoice_id(field)B2Brouter’s provider invoice id for the legal artefact.
PaTransmissionretrieval_timestamp(field)when the legal artefact was fetched and content-hashed.
PaTransmissionlegal_download_path(field)B2Brouter download_legal_url / /as/legal path the artefact was fetched from.
PaTransmissionvalidation_version / rendering_version(fields)the validation ruleset and renderer versions in effect at transmission, for provenance.
PaTransmissionaudit_metadata(field)provider/audit metadata (request id, status history, validation output) accompanying the stored artefact.
PaymentAllocationinvoice_idInvoiceAR collection (money in) only; seller-side. There is no bill_id — AP acquisition reporting is a separate bills-owned record.
PaymentAllocationtransaction_idBankTransactionthe real bank movement this entry allocates.
PaymentAllocationcorrection_reversal_link?PaymentAllocation (self)nullable; for a correction/reversal, the ledger entry it amends (append-only).
ReportableAcquisitionbill_idBillAP buyer-side acquisition e-reporting (CGI art. 290, Flux 10 acquisition data). No invoice_id — this is acquisition data, not AR payment data.
ReportableAcquisitionorganisation_idOrganisation / enrollmentevery reportable acquisition is org-scoped.
InvoiceTxMatchinvoice_id, transaction_idInvoice, BankTransactionjunction; incoming credit (AR)
BillPaymentbill_id, transaction_idBill, BankTransactionjunction; outgoing debit (AP)
ComplianceEventLog(no business FK)append-only outbox/audit; correlated by correlation_id / causation_id, not foreign-keyed into the graph.

Invariant — PaTransmission direction gates which reference is set. direction = outbound ⇒ invoice_id IS NOT NULL AND bill_id IS NULL; direction = inbound ⇒ bill_id IS NOT NULL AND invoice_id IS NULL. No transmission ever references both an AR invoice and an AP bill. This is the data-layer enforcement of the AR/AP boundary (see 10. Integration Contracts §8.1). PaymentAllocation is AR-only (a single invoice_id, seller-side collection); it does not carry a bill_id.

Design rule — the AR/AP boundary holds in the data model. An Invoice (AR) and a Bill (AP) share no table and no FK; the only things both touch are the PaTransmission (transport — which references exactly one side per direction) and the Organisation / BankTransaction foundation. The PaymentAllocation ledger is AR-only (seller-side collection). Outbound = Invoice → PaTransmission; inbound = PaTransmission → Bill.

Design rule — the issued-invoice legal artefact is stored on the outbound transmission. For an outbound transmission, PaTransmission persists the legal-archive fieldslegal_artifact_ref, content_hash, provider_invoice_id, retrieval_timestamp, legal_download_path (B2Brouter download_legal_url / /as/legal), validation_version, rendering_version, and audit_metadata. The stored artefact is the compliance copy; on-the-fly regeneration from the immutable invoice data is a display convenience only, never the legal archive. These fields stay consistent with 8. Archiving §5.2.

Design rule — raw provider payloads follow field-level retention. raw_payload_hash and any raw provider payload on PaTransmission follow the field-level retention rules in 13. Privacy and Data Protection: hash-only raw evidence for the audit trail, a short-TTL encrypted vault for any full raw payload retained for support, and secrets / signed download URLs are never persisted.

Design rule — the current account is org-level; the transmission keeps an immutable snapshot. The current b2brouter_account_id lives on the organisation / enrollment (the account we transmit through now; mutable, follows provider migration). Each PaTransmission additionally persists b2brouter_account_id as an immutable historical snapshot of the account that actually carried that exchange — captured once at submission/receipt time and never updated. It is not a mutable FK to the current account: resolving the current org account would silently rewrite history after a migration and break the legal trail. The snapshot exists for audit, replay, and provider migration (see 10. Integration Contracts §8).

Design rule — B2Brouter is external. The persisted links to B2Brouter are b2brouter_account_id (the current account on the org/enrollment, plus the immutable snapshot on each transmission) and b2brouter_invoice_id / b2brouter_document_ids (on each transmission). Everything else is our canonical model; the B2Brouter mapping lives in the adapter (5. Lifecycle Statuses §1.1).

5. Reading the two flows off the schema

6. Related Documents

13. Privacy and Data Protection

Privacy and Data Protection

This document is the privacy appendix to the archiving model. 8. Archiving and Audit Trail §2 establishes four distinct retention bases for the legal invoice artefacts and their evidence chain: (a) statutory VAT (fiscal) retention — 6 years, (b) statutory accounting (commercial) retention — 10 years, (c) Green-Got’s product archive promise (availability beyond the statutory minimums, so a 15-year customer can still retrieve their earliest invoices) on its own purpose + lawful basis + controls, and (d) permanent non-PII audit evidence (hashes / PAF integrity chain). This appendix draws the lines those bases must not blur: it separates statutory legal-archive retention (a)/(b) and the product archive promise (c) from operational / personal-data retention, sets CNIL-aligned durations for the latter, and specifies how Green-Got handles data-subject rights, log minimisation, and its B2Brouter subprocessor relationship. The governing principle is data minimisation: keep personal and operational data only as long as a lawful basis requires, while preserving the mandatory invoice data the law compels Green-Got to keep — and treating the product archive promise (c) as a product basis, not a “legal obligation forever” over personal-data-bearing bytes.

1. Terminology

Terms common to the whole documentation set are defined once in 1. Reform Overview. This section defines only the terms specific to privacy and data protection.

2. Data Categories

Green-Got’s invoicing and archiving stack handles distinct categories of data, each with its own lawful basis and retention rule.

CategoryExamplesContains PII?Retention driver
Legal invoice contentIssued-invoice legal artefacts, incoming bill originals, signed quotes, immutable structured invoice data, content hashesYes (counterparty identity, line items)Statutory legal obligation — 6 yr VAT / 10 yr accounting; availability beyond that under the product archive promise (separate basis) (8. Archiving §2)
PII of contacts / signatoriesNames, emails, phone numbers, signatory identity and signing audit beyond what the invoice itself requiresYesMinimised — operational duration (§4)
Operational logsApplication logs, request traces, error logs, lifecycle-event telemetrySometimes (incidental)Legitimate interest — short, CNIL-aligned (§4)
Webhook payloadsRaw B2Brouter / PA callback bodies, transmission and status callbacksSometimesMinimised — keep audit-relevant fields, drop the rest (§4, §6)
Audit eventsPAF status history, transaction match, retrieval timestamps, provider/audit metadataIndirectly (tied to invoices)Statutory legal obligation for the window; non-PII integrity evidence (hashes, PAF chain) kept permanently (basis (d), 8. Archiving §2)

Design rule: A field’s category — not the table it happens to sit in — determines its retention. The same webhook may carry both an audit event (keep with the legal artefact) and incidental PII / raw payload (minimise). Separate them at ingestion rather than retaining the whole payload forever.

3. CNIL Data-Lifecycle Phases

CNIL structures personal-data retention into three successive phases. Green-Got maps its data onto them explicitly.

PhaseMeaningWhat lives here at Green-Got
Active baseData in active use to accomplish the current purpose; full operational accessIn-flight invoices and quotes, recent logs, live contact records
Intermediate archiveNo longer in active use but retains legal / dispute value; access restricted to authorised personnelThe legal invoice artefacts and PAF held under the statutory 6 yr VAT / 10 yr accounting obligation (CNIL cites invoicing data as a 10-year intermediate-archive example), plus PII retained only for the duration of a possible dispute
Definitive archiveLong-term preservation for intrinsic valueBeyond the statutory window: the legal invoice artefacts kept under Green-Got’s product archive promise (basis (c) — its own purpose, lawful basis, and offboarding/deletion controls) for the customer’s lifetime with Green-Got, and the non-PII integrity evidence (hashes, PAF chain) kept permanently (basis (d))

Design rule — active vs archive access separation. Once data leaves the active base, access narrows. “Narrows” is not abstract: it is the concrete archive access contract below (§3.1). Day-to-day operational tooling must not retain standing access to archived PII; the legal artefacts and their evidence chain are reachable only through the authorised paths the contract defines.

3.1 Archive access contract (RBAC, tenant isolation, self-service, break-glass)

This is the single source of truth for how the permanent archive is accessed; 8. Archiving §5.8 points here. document_vault retrieval (search + per-token audited share links) operates under this contract.

ElementContract
Roles & scopes (RBAC)Access is granted by explicit role/scope, never ambient. Reading an archived legal artefact requires a scope that maps to a lawful basis; no role has blanket read over all tenants.
Tenant isolationEvery artefact and access path is bound to its owning tenant (customer). A query can only ever resolve artefacts within the caller’s tenant; cross-tenant reach is structurally impossible, not merely filtered.
Customer self-serviceA customer has first-class, self-service read access to their own archive (their issued invoices, received bills, signed quotes) and their evidence chain — the 15-year-retrieval promise is exercised here.
Support limitationsSupport staff can see operational metadata (status, ids, timeline) needed to help, but not standing access to the legal artefact bytes or archived PII. Any artefact-level access by support is via the break-glass path, logged.
Auditor share linksAn auditor reaches a customer’s archive through per-token share links with a TTL (expiry) and explicit revocation. Links are scoped to the specific artefacts/period shared, are revocable at any time, and every use is logged.
Break-glass workflowElevated, time-boxed access for incidents requires a mandatory reason capture (who, why, scope) recorded before access is granted; the grant is logged immutably and auto-expires.
Download limitsBulk/repeated downloads are rate- and volume-limited per role to bound exfiltration risk; export beyond the limit requires elevated authorisation.
Immutable access logsEvery archive read, share-link mint/use, and break-glass grant is written to an append-only, tamper-evident access log that is itself retained and auditable.
Periodic access reviewStanding roles/scopes are reviewed on a periodic cadence; access no longer justified by a lawful basis is revoked. Reviews are recorded.

Invariant: No access path may bypass tenant isolation or the immutable access log. Break-glass and auditor share links are the only ways to reach archived artefacts outside a customer’s own self-service scope, and both are TTL-bound, reason-captured (break-glass) or revocable (share links), and fully logged.

4. Retention: Legal vs Operational

This is the core reconciliation between 8. Archiving’s “permanent” rule and data minimisation.

Invariant — separate the statutory obligation, the product promise, and the permanent non-PII evidence. The legal invoice artefacts and their evidence chain (§2, category “Legal invoice content” and “Audit events”) are retained under the statutory legal-obligation basis for the 6-year VAT / 10-year accounting window (settled in 8. Archiving §2 and not relaxed here). Availability beyond that window for the full PII-bearing artefact rests on Green-Got’s product archive promise (basis (c)) — a distinct purpose and lawful basis with its own access controls and offboarding/deletion policy — not an indefinite legal obligation. Only the non-PII integrity evidence (content hashes, PAF chain — basis (d)) is kept permanently outright. Full personal-data-bearing invoice bytes are not declared “kept forever under legal obligation” without legal review.

Design rule — operational data is minimised. Everything that is not a legal invoice artefact — operational logs, raw webhook payloads, and PII of contacts/signatories beyond what the invoice itself records — is retained only for a defined operational duration, then deleted or anonymised. The targets below are CNIL-aligned but indicative: each duration must be confirmed against the specific in-force CNIL référentiel / recommandation cited and documented per the presumption-of-compliance rule. Confirming and pinning each duration to its référentiel is a launch-gate uncertainty — tracked as W-4 (see uncertainties.md); the values here are the working defaults until W-4 closes.

DataIndicative operational retentionBasisCNIL référentiel / source
Application / request logs (connection/access logs)~6–12 months, then deletedLegitimate interest (security, debugging)CNIL recommandation journalisation — access/connection logs on a sliding 6 months–1 year window (extendable only for legal obligation / litigation / incident analysis)
Raw webhook payloads (beyond audit fields)Reduced to audit-relevant fields at ingestion; raw body purged shortly after processingMinimisationCNIL data-minimisation principle (durées de conservation); category (3) short-TTL vault (§4.1)
PII of contacts / signatories not part of invoice contentActive relationship + dispute window (CNIL: 3 years from end of relationship / last contact for prospects)Contract / legitimate interestCNIL référentiel gestion des activités commerciales (délibération n° 2021-131, 23 sept 2021) — clients/prospects 3-year window
Legal invoice artefacts + PAF (full PII-bearing bytes)6 yr VAT / 10 yr accounting statutory, then product archive promise for the customer lifetimeStatutory legal obligation (a)/(b), then product promise (c) — own purpose + lawful basis + controlsCNIL cites billing data = 10 years (Code de commerce) as an intermediate-archive example; statutory floor per 8. Archiving §2
Non-PII integrity evidence (content hashes, PAF chain)PermanentLegitimate interest — tamper-evidence (basis (d))Non-PII; outside CNIL personal-data retention scope

Invariant: No operational-minimisation rule may delete or alter a legal invoice artefact, its content hash, or its audit metadata. Minimisation operates strictly on data outside the legal evidence chain. When a webhook or log entry contributes audit evidence, the audit-relevant fields are extracted into the permanent PAF before the raw record is minimised.

4.1 Field-level retention table

“Minimise raw payloads” is not a single rule — it is a per-field classification. Every field Green-Got persists falls into exactly one of the five categories below; the category fixes how long the field lives and in what form. This table is the single source of truth other documents point to for raw-payload handling.

#CategoryWhat it coversStorage formLifetime
1Permanent canonical audit fieldsThe legal-artefact contract fields (8. Archiving §5.2): content_hash, provider_invoice_id, retrieval_timestamp, tax-report id, CDAR / status history, validation output, request idStored verbatim, structuredPermanent (legal obligation)
2Hash-only raw payload evidenceRaw provider payloads kept for tamper-evidence — the hash, not the body (e.g. raw_payload_hash of a status callback)Store the hash only; discard the bodyHash kept with the PAF (permanent); body not retained
3Encrypted short-TTL raw vaultThe full raw body, where retained at all for provider support / dispute investigationEncrypted vault, access-controlledShort TTL — auto-expires; never permanent
4Never-persistSecrets, signed download URLs, signatures, API keys/tokensNever written to durable storage in any formNever persisted
5Redaction before persistenceFields that must be masked before they are stored: PANs, IBAN metadata, emails, refusal-reason free textMasked/truncated at the point of captureMasked value only; raw value never durably stored

Design rule — classify at ingestion, not later. A field’s category is decided as it is ingested, before anything durable is written. Category (4) values must never reach a log buffer or row in clear (see §6 invariant); category (5) values are masked before persistence; category (2) bodies are hashed-and-dropped; category (3) bodies go straight to the encrypted short-TTL vault and nowhere else. Only category (1) is permanent.

Category (3) — TTL-start semantics, RBAC, and storage shape. The short-TTL encrypted vault holding raw bodies needs three things pinned beyond its duration:

Launch gate — exact TTLs are a first-real-traffic gate, not an open item. The category structure above is settled, but the exact TTLs for category (3) (the short-lived encrypted support/dispute vault holding raw bodies and any retained request_log_url) and for operational logs are launch-blocking on first real traffic: the precise durations must be decided, configured, and enforced (auto-expiry verified) before the first regulated transmission for a real customer. This is a closure gate, not a “nice to have later” — see uncertainties.md W-3. The permanent store keeps only normalised request ids / provider ids / codes / hashes (category (1)/(2)); raw bodies and request_log_url live only in the short-TTL category (3) vault; signed bearer URLs, secrets, and signatures are category (4) and are never durable in any store, regardless of TTL.

Reconciliation with the integration contracts. The named raw fields elsewhere in the docs follow this table, and none of them is ever permanent:

5. Data-Subject Rights and the Legal-Obligation Exception

Green-Got honours DSARs (access, rectification, erasure, portability, restriction) for personal data under its control.

Design rule — right-to-erasure has a legal-obligation exception, bounded to the statutory window. A data subject’s request to erase personal data does not override the legal obligation to retain mandatory invoice data for the statutory period (6 yr VAT / 10 yr accounting). Where personal data is embedded in a legal invoice artefact (e.g. a counterparty’s name and address on an issued invoice), that data is retained despite an erasure request for the statutory window — GDPR Art. 17(3)(b) (processing necessary for compliance with a legal obligation). The response to the data subject explains the exception and its scope.

Beyond the statutory window the legal-obligation exception no longer applies. Continued availability of the full PII-bearing artefact then rests on the product archive promise (basis (c), 8. Archiving §2), which is not a legal obligation and therefore does not by itself defeat an erasure request. The product-archive deletion / offboarding policy and the customer’s own controls govern what happens once the statutory period elapses; an erasure or account-closure request handled under that policy may remove the personal-data artefact while the non-PII integrity evidence (hashes, PAF chain — basis (d)) is retained. The operative customer-exit (account-closure / unsubscribe) and post-statutory DSAR mechanics — export window, statutory-floor preservation, post-statutory default-delete — are defined in the Product Archive Promise policy (8. Archiving §5.9); this section is its privacy-side counterpart.

Erasure does apply to personal data held outside the legal evidence chain: operational logs, surplus contact records, and PII retained only on a minimisation basis are erased or anonymised on a valid request, subject to any live dispute.

Design rule — active vs archive on erasure. Following CNIL’s intermediate-archive model, erasure first removes the data from the active base; data still subject to a legal-retention obligation remains only in the restricted-access archive for the mandated period, not in operational systems.

6. Redaction, Minimisation, and Log Masking

Design rule — mask secrets and PII in logs. Logs, traces, and stored webhook payloads must mask sensitive values at the point of capture: PANs, tokens, API keys and secrets, and personal identifiers are redacted (or never logged) so operational telemetry carries no unnecessary sensitive data. This is both a privacy control (data minimisation) and a security control (see also the PCI DSS documentation set).

Invariant: Masking is applied before persistence, not as a later scrubbing pass — a secret or PAN must never reach durable storage in clear, even transiently in a log buffer that is later cleaned.

7. B2Brouter DPA and Subprocessor Responsibilities

B2Brouter (the PA) processes personal data on Green-Got’s behalf when transmitting invoices and is a subprocessor under GDPR.

Design rule — bind the subprocessor by DPA. A data processing agreement must bind B2Brouter to: processing only on Green-Got’s documented instructions; appropriate security measures; assistance with DSARs and breach notification; restrictions on onward subprocessing; and deletion or return of personal data at end of service. This complements the archiving-side contractual terms (retrieval SLA, export rights, exit plan, checksum strategy, customer access) in 8. Archiving §5.6.

Invariant — controllership and liability stay with Green-Got. Using B2Brouter as a processor does not transfer Green-Got’s responsibility as controller, just as delegating storage does not transfer the archiving obligation (see 8. Archiving §7 and 3. Actors and Legal Posture). Green-Got remains accountable for lawful basis, retention, minimisation, and data-subject rights across the whole chain.

7.1 Signed-DPA launch gate (Article 28 closure)

The generic “must bind by DPA” rule above is not self-evidencing: a launch checklist that reads only that rule could send real customer data before the signed agreement and its coverage are actually recorded and reviewed. This sub-section is the recorded launch gate that closes that hole. Binding B2Brouter by a signed Article 28 DPA is the intended posture (tracked in uncertainties.md L-6); the fields below capture the reference and its coverage so the gate can be checked against recorded evidence, not merely assumed. “DPA intended” is not “DPA evidence verified”: the signed reference and a reviewed coverage matrix are not yet recorded, so the gate below is OPEN.

STATUS: OPEN — NOT CLOSED. Launch-blocking. The signed-DPA evidence (contract id, owner, signed date, coverage-review date, linked artifact) and the checked coverage matrix below are not yet recorded. The placeholders are unfilled. A launch reviewer must not read “a DPA must be in place” as “DPA evidence verified”. No real invoice PII may be sent to B2Brouter until every placeholder below is filled with the real signed-DPA evidence and every coverage row is checked and confirmed.

Launch gate (blocking). No first real customer traffic / no real invoice PII to B2Brouter until the signed DPA reference is recorded below and its coverage matrix is reviewed and confirmed. This is launch-blocking on the first regulated transmission for a real customer, consistent with the convention in uncertainties.md. Until then the gate is OPEN.

Recorded signed-DPA reference — STATUS: OPEN (evidence not yet recorded; all rows are unfilled placeholders).

FieldValue
Gate statusOPEN — evidence not yet recorded; launch-blocking
Signed Article 28 DPA reference (doc / contract id)‹UNFILLED — to record: DPA document or contract identifier›
Owner (accountable for record + review)‹UNFILLED — to record: DPO / Legal owner›
Signed / recorded date‹UNFILLED — to record: YYYY-MM-DD›
Coverage reviewed by / date‹UNFILLED — to record: reviewer + YYYY-MM-DD›
Linked signed-DPA artifact‹UNFILLED — to record: link to the executed DPA document›

Coverage matrix — STATUS: OPEN. Every item is UNCHECKED (☐) and must be confirmed present in the signed DPA before the gate clears. This is the GDPR Art. 28(3) checklist; departing from any line requires documented justification (see CNIL subcontracting guidance). The boxes below are unchecked because the signed-DPA evidence has not been recorded or reviewed.

Coverage itemWhat the DPA must bind B2Brouter to
Processing on documented instructionsB2Brouter processes personal data only on Green-Got’s documented instructions (Art. 28(3)(a)).
Security measuresAppropriate technical and organisational security measures (Art. 28(3)(c), 32).
DSAR assistanceAssistance in responding to data-subject access / rectification / erasure / portability requests (§5; Art. 28(3)(e)).
Breach notificationTimely notification to Green-Got of personal-data breaches (Art. 28(3)(f), 33).
Onward-subprocessor limitsNo onward subprocessor without authorisation, flow-down of equivalent obligations (Art. 28(2), 28(4)).
Deletion / return at end of serviceDeletion or return of personal data at end of service, subject to Green-Got’s statutory-retention needs (Art. 28(3)(g)).
Audit rightsGreen-Got’s right to audit / receive evidence of compliance (Art. 28(3)(h)).
Data locationDocumented processing locations and a lawful transfer basis for any processing outside the EEA.

Invariant — recording the gate does not relax §7 above. The signed reference and reviewed coverage matrix are the evidence that the §7 design rule and the controllership/liability invariant are met; they do not transfer controllership to B2Brouter. The evidence is not yet recorded: the reference table holds only unfilled placeholders and every coverage box is unchecked, so the gate is OPEN and the no-real-traffic rule applies in full. The gate closes — and only then may real invoice PII flow to B2Brouter — once every placeholder above is filled with the real signed-DPA evidence and every coverage row is checked and confirmed.

8. Related Documents

9. Sources

14. Implementation Wiring

Implementation Wiring — Target Matrix

Purpose. This document is the target implementation wiring plan for Green-Got’s French e-invoicing stack: how the three crates (invoicing, bills, plateforme_agreee) are intended to be assembled into running code — crates and dependencies, stores, use cases, adapters, Temporal workflows/activities, eventbus rules, public/private routers, feature flags, and the ownership of each command and event. It is the bridge between the design docs (which say what the system does and who owns what fact) and the build.

This is a PLAN, not built state. Every crate referenced here is currently scaffolding only — empty lib.rs, structural Cargo.toml, phased plan.md, and these docs. No production code exists yet (see business_domain/readme.md §Status). Names of modules, stores, events, workflows and routes below are the intended wiring; they are authoritative as a target but must not be read as “already implemented”. Settled decisions this doc does not re-litigate: the three-crate split (invoicing AR / bills AP / plateforme_agreee transport+compliance), the use of Temporal for long-running flows, and the eventbus contract canonically defined in 10. Integration Contracts.

Canonical models referenced generically throughout (defined elsewhere, never redefined here): the payment-allocation / VAT-collection ledger (10. Integration Contracts §11), the ComplianceEventLog / outbox (10. Integration Contracts §12), MandateEvidence / MandateConsent (9. Mandate and Onboarding), and the PaTransmission schema (10. Integration Contracts §8.1, 12. Data Model).

1. Crate Layout and Dependencies

Three independent workspace members under src/business_domain/, plus the shared substrate they rest on. None of the three depends on the others at compile time for business logic — they are coupled only through the eventbus (each owns a static EventBus<CrateEvent>; consumers subscribe via rules/).

CrateRoleCompile-time deps (intended)Coupling to siblings
invoicing (AR)Issued invoices, quotes, numbering, payment links, reminders, AR matching, the allocation ledger writer.organisation (OrganisationId), core_banking (BankTransaction), object_storage, yousign (quote signature), Temporal, eventbus.Emits InvoiceFinalized, TransactionMatched, PaymentCollected; consumes TransmissionStatusChanged.
plateforme_agreee (transport + compliance)Transmissions (outbound + inbound), B2Brouter adapter, format mapping, webhook/poll ingestion, authoritative legal status, ComplianceEventLog, Flux 10 overlay (payment + acquisition), the MarkOutboundInvoicePaid (AFNOR 212) push, enrollment link.organisation, object_storage, core_banking (read), Temporal, eventbus, the B2Brouter HTTP client.Consumes InvoiceFinalized, TransactionMatched, PaymentCollected, ReportableAcquisitionRecorded; emits TransmissionStatusChanged, InboundInvoiceReceived.
bills (AP)Received supplier invoices, supplier master, approval workflow, SEPA scheduling, AP matching, OCR for non-PA channels, the reportable-acquisition record.organisation, core_banking (SEPA + matching), object_storage, Temporal, eventbus, OCR adapter.Consumes InboundInvoiceReceived; emits the AP acquisition-data event ReportableAcquisitionRecorded.

Design rule — no sibling compile-time dependency for business logic. invoicing and bills never use plateforme_agreee::… types in their domain layer; the handoff is the canonical structured invoice (4. Formats and Invoice Data) and the eventbus payloads, both of which carry ids/references, not aggregates. This is what keeps the AR/AP/transport boundary enforceable.

Design rule — the en16931 module is shared transport vocabulary. plateforme_agreee::en16931 (the only module that exists in scaffolding today) holds the EN 16931 BT/BG term model and Factur-X profile selection. invoicing populates the canonical structured invoice; plateforme_agreee maps it to B2Brouter JSON. The mapping never leaks into invoicing.

2. Onboarding / Enrollment — the Prerequisite Phase

No transmission (outbound or inbound) is possible until enrollment completes. Enrollment is an explicit prerequisite phase, owned by plateforme_agreee, gating every other flow. It runs once per Organisation (one per SIREN) and is a durable, resumable Temporal workflow because each step calls an external system and any can fail or need human follow-up.

sequenceDiagram
    autonumber
    participant ORG as organisation
    participant PA as plateforme_agreee
    participant B2B as B2Brouter
    participant DGFiP as DGFiP / Annuaire

    ORG-->>PA: Org KYB complete, customer consents to e-invoicing
    PA->>PA: Capture MandateConsent + MandateEvidence (legal wording, timestamp, actor)
    PA->>B2B: Create B2Brouter account for the SIREN
    B2B-->>PA: b2brouter_account_id
    PA->>B2B: POST tax_report_settings (enable DGFiP / Flux 1 + Flux 10)
    B2B->>DGFiP: Register SIREN in the Annuaire as routed-to-B2Brouter
    DGFiP-->>B2B: Annuaire registration acknowledged
    B2B-->>PA: Account DGFiP-enabled, designated PA = B2Brouter
    PA->>PA: Persist enrollment (b2brouter_account_id, status=Active), append ComplianceEventLog
    Note over PA: Only now may outbound/inbound transmission run for this org.
StepOwnerProduces / persistsCanonical model
1. Mandate captureplateforme_agreee enrollment use caseMandateConsent (org-level, status: active) + MandateEvidence (exact legal wording rendered, timestamp, acting member, IP/device)9. Mandate and Onboarding
2. B2Brouter account creationplateforme_agreee → B2Brouter adapterb2brouter_account_id on the enrollmentb2brouter/3. Onboarding Accounts
3. DGFiP tax-report settingsplateforme_agreee → B2Brouter adaptertax_report_settings enabled (Flux 1 B2B + Flux 10 e-reporting)b2brouter/3. Onboarding Accounts
4. Annuaire registrationB2Brouter (on Green-Got’s behalf)SIREN routed-to-B2Brouter in the Annuaire; B2Brouter becomes the designated PA6. Annuaire and Routing
5. Enrollment finalizeplateforme_agreeeenrollment Active; ComplianceEventLog record10. Integration Contracts §12

Invariant — enrollment gates transmission. The outbound rules/ handler that consumes InvoiceFinalized and the inbound poll/webhook ingestion both first resolve the org’s enrollment; if it is not Active, the work is parked (not failed) until enrollment completes. Enrollment is a phase before the transmission matrix of §3, not a step inside it.

Enrollment error states, stall detection, and mandatory failure alert. The happy path above settles to Active; resumability/parking of in-progress steps is already specified (the durable workflow retries each external call). Two things the wiring adds explicitly:

3. Command and Event Ownership

Every command (a thing a router or workflow invokes) and every cross-crate event maps to exactly one owning crate. The event contract is canonical in 10. Integration Contracts §6; this table adds the command side and the owning use case.

Command / EventKindOwner crateUse case / handlerNotes
EnrollOrganisationcommandplateforme_agreeeuse_cases::enroll (drives the §2 workflow)Prerequisite phase; idempotent per SIREN.
FinalizeInvoicecommandinvoicinguse_cases::finalize_invoiceAssigns gap-free number, freezes the AR record, emits InvoiceFinalized.
InvoiceFinalizedeventinvoicingconsumed by plateforme_agreee::rulesBegins the outbound contract.
SubmitTransmissioncommandplateforme_agreeeuse_cases::submit_outboundMaps canonical invoice → B2Brouter JSON, creates PaTransmission, calls B2Brouter.
TransmissionStatusChangedeventplateforme_agreeeconsumed by invoicing::rulesAR invoice updates its projection of the legal status.
IngestInboundcommandplateforme_agreeeuse_cases::ingest_inboundFrom poll or webhook hint; persists inbound PaTransmission, downloads original.
InboundInvoiceReceivedeventplateforme_agreeeconsumed by bills::rulesbills creates the AP Bill (Received state; “structured source / no OCR” is extraction metadata, not a Bill.status).
MarkReceivedAccepted / MarkReceivedRefusedcommandbills (decision) → plateforme_agreee (push)bills::use_cases::decide_received → PA mark_asThe only confirmed received-French mark_as targets.
TransactionMatchedeventinvoicingconsumed by plateforme_agreee::rulesFull-amount AR settlement; drives the commercial axis → Paid (commercial-axis terminal; AFNOR-legal Settled/Encaissée is a separate axis), not Flux 10. Triggers MarkOutboundInvoicePaid and/or PaymentCollectedledger write + PaymentCollected first, MarkOutboundInvoicePaid after (see ordering below).
MarkOutboundInvoicePaidcommandplateforme_agreeeuse_cases::mark_outbound_paidPushes AFNOR 212 (Encaissée) to the recipient’s PA when legally applicable (CGI art. 290 A / VAT-on-collection). Triggered by TransactionMatched but sequenced after the PaymentCollected ledger write; references it by allocation_correlation_id so the enriched 212/MEN carries the collected payment data. Distinct from PaymentCollected (different legal projection of the same collection).
PaymentCollectedeventinvoicing (ledger writer)consumed by plateforme_agreee::rulesOne allocation-ledger entry written by invoicing; the immutable data source for payment data when a breakdown group is reportable, carried channel-specifically (Enriched212Men for invoiced Flux-1 ops, Flux10 only for non-invoiced).
ReportableAcquisitionRecordedeventbills (AP)consumed by plateforme_agreee::rulesOne reportable-acquisition record (CGI art. 290); feeds Flux 10 acquisition data. Keyed on bill_id; not payment data, not PaymentCollected.
ReportPaymentDatacommandplateforme_agreeeuse_cases::report_flux10Overlay push for reportable ledger entries (AR payment data) and reportable acquisitions (AP acquisition data).

Design rule — collection ordering: ledger write → PaymentCollectedMarkOutboundInvoicePaid. When one bank settlement produces both a collection-ledger entry and the AFNOR 212 push, the wiring sequences them so the enriched 212/MEN can be correlated to the payment data (canonical invariant in 10. Integration Contracts §6): (1) invoicing writes the allocation-ledger entry (its AllocationLedgerStore, §4) in the same DB transaction as its ComplianceEventLog record; (2) that write emits PaymentCollected carrying the allocation group id(s) / deterministic correlation id; (3) only then is MarkOutboundInvoicePaid dispatched, referencing that allocation_correlation_id (or plateforme_agreee reads the ledger by it) so the 212/MEN carries the right collection date + amount-by-VAT-rate. A bare 212/MEN push must never fire before the correlated allocation data exists, and the correlation id makes the push idempotent (no out-of-order or duplicate report). invoicing owns the allocation-ledger write; plateforme_agreee only reads the ledger and owns the reporting/submission projections.

Design rule — buyer decision vs transport push are two owners. The accept/refuse business decision on a received invoice is a bills command; the mark_as transport push to the supplier is a plateforme_agreee action mediated on the transmission. invoicing is never involved in a received-invoice path (10. Integration Contracts §4).

4. Stores

DB access uses sqlx::query! macros in each crate’s stores/ (or infrastructure/stores/) layer per the repo DDD conventions (architecture skill). Ownership of each table follows the entity ownership in 12. Data Model.

StoreCrateBacks entityCanonical ref
EnrollmentStoreplateforme_agreeeenrollment (b2brouter_account_id, status), MandateConsent, MandateEvidence9. Mandate and Onboarding
TransmissionStoreplateforme_agreeePaTransmission (direction-aware, immutable account snapshot)10. Integration Contracts §8.1
ComplianceEventLogStoreplateforme_agreeethe append-only outbox10. Integration Contracts §12
InvoiceStore / QuoteStoreinvoicingissued invoices, credit notes, lines, numbering sequencesinvoicing/3, invoicing/5
AllocationLedgerStoreinvoicingthe append-only payment-allocation / VAT-collection ledger10. Integration Contracts §11
BillStore / SupplierStorebillsreceived invoices, supplier master, approval state, bill paymentsbills/2
ReportableAcquisitionStorebillsthe buyer-side reportable-acquisition record (AP, CGI art. 290; keyed on bill_id)12. Data Model §2–§4

Invariant — append-only stores never UPDATE/DELETE. ComplianceEventLogStore and AllocationLedgerStore only ever INSERT; corrections are reversing/superseding rows linked by correlation_id / correction_reversal_link. The original document artifact for inbound is stored in core S3 (bills/ subpart), not in these stores (10. Integration Contracts §4).

5. Adapters

External HTTP clients live in infrastructure/adapters/<provider>/. The B2Brouter adapter is the only provider unique to plateforme_agreee.

AdapterCrateWrapsMapping responsibility
B2Brouter HTTP clientplateforme_agreeeaccount create, tax_report_settings, POST invoices, GET invoices/{id}, mark_as, /invoices/import, webhooks*_from/to_b2brouter_data mappers; canonical model ⇄ B2Brouter JSON; pins FR wire values at the mapper layer only.
Format/EN 16931 mapperplateforme_agreee (en16931)EN 16931 BT/BG terms, Factur-X profilecanonical structured invoice → format terms; validated against the EN 16931 / FNFE-MPE spec at generation.
Yousign clientinvoicing (shared src/yousign/)quote signatureextracted to a top-level crate before signature ships (readme §Shared yousign crate).
OCR clientbillsstructured extraction for email/upload channelsPA-delivered invoices skip OCR (arrive structured).

Design rule — wire values stay in the adapter. Every B2Brouter-specific value (tin_scheme, EAS 0225, error codes, tax_report_settings body, API version) is confined to the adapter/mapper layer. The canonical internal model never carries a B2Brouter value (12. Data Model §4). The exact values that are not yet staging-confirmed are tracked in uncertainties.md (the active register).

6. Temporal Workflows and Activities

Temporal carries every long-running, externally-dependent, or human-in-the-loop flow. Each crate registers its workflows/activities in its service.rs. The Business API service itself currently registers no Temporal workers (temporal: vec![] in business_api::service.rs); the e-invoicing crates register their own when built.

WorkflowCrateTriggersActivities (external calls)
EnrollmentWorkflowplateforme_agreeeEnrollOrganisationaccount create, tax_report_settings, Annuaire registration, finalize
OutboundTransmissionWorkflowplateforme_agreeeInvoiceFinalized (post-enrollment)map+submit to B2Brouter, durable retry under the rate budget
InboundPollWorkflowplateforme_agreeeTemporal schedule — hourly (+ webhook hint as early-poll trigger)poll received list, GET invoices/{id}, download originals, dedupe on the B2Brouter invoice id
StatusReconcileWorkflowplateforme_agreeeschedule + webhook hintreconstruct AFNOR status from raw + CDAR, emit TransmissionStatusChanged
DesignationSweepWorkflowplateforme_agreeeTemporal schedule — at least dailysweep active enrollments via Annuaire/directory lookup; on a SIREN no longer resolving to our account, set inbound_designation_lost_at and fire the mandatory loss-of-designation alert (9. Mandate §5.5)
Flux10ReportingWorkflowplateforme_agreeePaymentCollected (reportable AR payment data) and ReportableAcquisitionRecorded (AP acquisition data)overlay push (payment + acquisition); correction re-report
QuoteSignatureWorkflowinvoicingquote sendYousign signature, escalating levels
ReminderWorkflowinvoicingper-document configscheduled reminder cadence
BillApprovalWorkflowbillsInboundInvoiceReceived / uploadapproval routing, SEPA scheduling

Design rule — inbound polling is authoritative on a fixed cadence (decision D3). Polling is the system-of-record for inbound state (b2brouter/5 §4.2, 10. Integration Contracts §4.1); the webhook is a hint that triggers an early poll, never the source of truth. The wiring:

Inbound rehearsal requirement (modeled on the provider-exit rehearsal, 8. Archiving §5.7). Before relying on inbound in production, the inbound path is rehearsed end-to-end on staging, not improvised live:

Webhook-elevation acceptance criteria. The escape clause “promote the webhook to authoritative if B2Brouter delivery proves reliable enough” is operationalized, not left vague — webhooks may be elevated above hourly polling only when all of:

  1. A documented B2Brouter webhook delivery SLA exists (delivery guarantee, ordering, at-least-once semantics) — provider-supplied, not assumed.
  2. A staging test demonstrates that SLA over a representative window (no lost deliveries, dedupe holds, signature verification stable — b2brouter/6 §4).
  3. The elevation is written into the PA contract with B2Brouter (so the reliance is contractual, not best-effort).

Until all three hold, polling remains authoritative and the webhook stays a hint. This criterion is tracked against the open inbound-channel item in uncertainties.md.

Design rule — the outbound transmission worker is the rate gate. Every outbound transmission is a durable Temporal queue entry dispatched as soon as the B2Brouter rate budget allows and retried until terminal success or a terminal business error (b2brouter/7. API Mechanics). The exact ceilings are tracked in the active register until staging-confirmed.

6.1 B2Brouter provider-outage degraded mode

B2Brouter is a single external dependency (no backup PA at MVP — see below). A B2Brouter outage does not have one uniform impact: outbound carries legal-deadline exposure; inbound carries visibility latency. The degraded-mode behaviour splits accordingly:

7. Eventbus Rules

Each crate’s rules/ module registers Rule<T> subscriptions against sibling EventBus<T> statics in its Service::register. The full rule wiring:

Subscriber crate (rules/)Subscribes to eventOwned byAction
plateforme_agreeeInvoiceFinalizedinvoicingstart OutboundTransmissionWorkflow (after enrollment gate)
plateforme_agreeePaymentCollectedinvoicingfeed Flux 10 payment data for reportable entries
plateforme_agreeeReportableAcquisitionRecordedbillsfeed Flux 10 acquisition data (AP, CGI art. 290; keyed on bill_id)
plateforme_agreeeTransactionMatchedinvoicingtrigger MarkOutboundInvoicePaid (AFNOR 212, when legally applicable); no Flux 10 directly; also consumed for reconciliation context
invoicingTransmissionStatusChangedplateforme_agreeeupdate the AR invoice’s legal-status projection
billsInboundInvoiceReceivedplateforme_agreeecreate the AP Bill (Received; structured-source/no-OCR recorded as extraction metadata, not a status), start BillApprovalWorkflow

Invariant — events carry ids, not aggregates. Consumers re-resolve from the owning crate or download via a document_ref; this is what keeps ownership clean (10. Integration Contracts §6).

8. Routers — Public / Private

Routes contain no business logic; they call use cases (project CLAUDE.md rule). The Business API (src/services/business_api/) is the HTTP/RPC presentation layer for the mobile and web business apps. It composes a private_routes() group (behind session_auth_middleware) and a public_routes() group (no auth), merged into one OpenAPI document served at /business (see business_api::service.rs).

Router groupVisibilityCrate-backed use casesNotes
invoicing::router()private (already merged in private_routes())issue/finalize invoices, quotes, payment links, remindersThe one e-invoicing router currently wired into business_api.
bills::router() (intended)privatelist/approve/pay received bills, accept/refuseTo be merged into private_routes() when built.
plateforme_agreee enrollment routes (intended)privatestart/inspect enrollment, mandate consent captureCustomer-facing enrollment lives behind auth.
B2Brouter webhook endpoint (intended)public (HMAC-verified, not session)inbound notification hintVerified by X-B2Brouter-Signature HMAC, not session auth; a poll-triggering hint only (10. Integration Contracts §4).

Design rule — the webhook is public-but-authenticated-by-HMAC. It cannot sit behind session_auth_middleware (B2Brouter has no session); it is a public route whose authenticity is the constant-time HMAC check, and it is treated as a hint, never the system of record.

9. Feature Flags

The server (src/server/src/lib.rs) registers each Service behind a Cargo feature (#[cfg(feature = "…")]). business_api is registered behind feature = "business_api". The e-invoicing crates, once they expose a Service, get their own feature gates and are registered in the same main() sequence.

Flag (intended)GatesStatus
business_apithe Business API HTTP service (hosts invoicing::router() today)exists
plateforme-agreeethe PA Service (enrollment + transmission workers, webhook route)to add when built
billsthe AP Service (approval workers, AP routers)to add when built
invoicing (worker side)invoicing Temporal workers (signature, reminders) beyond the API routerto add when built

Design rule — API surface and worker surface are separately gated (the two-registration pattern). Each e-invoicing crate exposes two independently-registrable surfaces, and they are wired in two distinct places behind two distinct gates:

  1. API (router) registration — hosted by business_api. The crate’s router() is merged into business_api’s private_routes() (or public_routes() for the HMAC webhook) and is therefore gated by feature = "business_api". This is the presentation surface; it ships whenever the Business API ships. invoicing::router() is already wired this way today.
  2. Worker (Service) registration — in server/src/lib.rs. The crate’s Temporal workers + eventbus rules/ are exposed as its own Service, registered in the server’s main() sequence behind the crate’s own feature gate (plateforme-agreee, bills, invoicing worker-side). This is the background-work surface; it can be enabled/disabled independently of the API.

So a single crate like invoicing has its router behind business_api and its workers (QuoteSignatureWorkflow, ReminderWorkflow) behind invoicing. The two registrations are not alternatives — most crates register both, in both locations. This split lets the HTTP surface and the durable-work surface be rolled out, scaled, and feature-flagged independently, mirroring how the existing services separate presentation from background work. The table above lists the worker-side gates; every crate’s router rides the business_api gate via business_api.

10. Build-Order Summary

  1. Enrollment phase first (§2) — without it no transmission can run; it owns MandateConsent / MandateEvidence and the b2brouter_account_id.
  2. Outbound contractinvoicing finalize → plateforme_agreee submit → status projection back.
  3. Inbound contract — poll/webhook ingest → InboundInvoiceReceivedbills Bill.
  4. Flux 10 overlay — AR allocation-ledger writes → PaymentCollected (payment data) and AP reportable-acquisition writes → ReportableAcquisitionRecorded (acquisition data) → Flux 10. Bank settlement also triggers MarkOutboundInvoicePaid (AFNOR 212) where legally applicable.

All four rest on the canonical models referenced at the top; none introduces a new source of truth. The open wire-level and legal items that must be resolved before launch are tracked in the active, classified register at uncertainties.md.

11. Related Documents

2. Platform Architecture

Platform Architecture

This document describes the post-2024 “five-corner” model of the French e-invoicing system and locates B2Brouter (the approved platform) and Green-Got (the compatible solution) within it.

1. Terminology

The canonical glossary lives in 1. Reform Overview. Terms used most heavily here:

2. The 2024 Architectural Revision

The original design (the “Y” / four-corner model) made the PPF a free public platform through which any company could exchange invoices directly. The 2024 revision removed the PPF’s invoice-exchange role, producing today’s five-corner model.

After the revision:

Design rule: Any model in Green-Got’s code that treats the PPF as a transmission endpoint, or that confuses the national Annuaire with a platform-local directory, reflects the obsolete pre-2024 design and must be corrected.

3. Roles and Responsibilities

3.1 PPF (Portail Public de Facturation)

3.2 PA (Plateforme Agréée)

3.3 OD / SC (Solution Compatible)

3.4 Annuaire

3.5 Roles comparison

ActorExchanges invoices?Talks to DGFiP?Certified?Green-Got relation
PPFNo (directory + concentrator only)Yes (aggregates and forwards)State-runIndirect, via the PA
PA (B2Brouter)Yes (sole intermediary)Yes (via PPF concentrator)Yes (DGFiP-registered, 3-year term)Green-Got’s transport provider
SC (Green-Got)No (through a PA)No (through a PA)NoGreen-Got itself
AnnuaireNo (routing lookup)n/aState-run (part of PPF)Queried by the PA on Green-Got’s behalf

4. Flux 1 vs Flux 10

The reform separates regulated data into two flows, both carried by the PA.

The two flows are not mutually exclusive per transaction. The transaction channel (which structured invoice, if any, is exchanged) and the payment-data overlay (CGI art. 290 A) are decided independently:

Invariant: Each operation is classified twice and independently — once for its transaction channel (Flux 1 domestic B2B, Flux 10 B2C/cross-border, or out of scope) and once for the payment-data overlay (carried on Flux 10, required iff VAT is due on collection and the operation is not reverse-charge). Payment data can therefore sit on top of a Flux 1 invoice. See 1. Reform Overview and 7. E-Reporting.

Design rule: When B2Brouter has DGFiP/PPF enabled for an account, it routes B2B domestic invoices over Flux 1 and e-reporting over Flux 10 automatically — for the invoice/transaction data there is no separate e-reporting API call from Green-Got. This does not cover the VAT-on-collection payment-data overlay (the payment-data overlay in the invariant above): that overlay is Green-Got-owned and its exact submission/correction mechanism is launch-blocked pending the confirmed B2Brouter mechanism (see 7. E-Reporting §6 and Uncertainties L-4, P-7). See B2Brouter integration and 7. E-Reporting.

4.1 Channel selection from the party model

The transaction channel (Layer 1 in 1. Reform Overview §3.3) is decided before finalization from the recipient’s party type. The party model is FrenchCompany | ForeignCompany | Individual | PublicEntity, and the channel algorithm is deterministic:

The payment-data overlay (Layer 2) is then decided orthogonally on top of whichever channel was selected — required iff VAT is due on collection and the operation is not reverse-charge — exactly as the invariant above states. The channel value, its emitted events, and the wiring align with the party model in 9. Onboarding / the data model docs.

5. Peppol

France became a Peppol national authority on 2025-07-08, with DGFiP as the authority. Peppol enables international interoperability over UBL/CII. B2Brouter is also a Peppol Access Point, so cross-border exchange to Peppol-reachable recipients is available through the same PA.

6. Five-Corner Routing Model

The diagram shows the routing of a domestic B2B invoice (Flux 1) from a Green-Got customer to its client, with the PPF concentrator and DGFiP receiving the reported data.

sequenceDiagram
    autonumber
    participant SC as Green-Got (SC)
    participant SPA as Sender PA (B2Brouter)
    participant AN as Annuaire (PPF)
    participant RPA as Recipient PA
    participant RC as Recipient (client)
    participant DG as PPF concentrator → DGFiP

    SC->>SPA: Submit invoice (JSON → Factur-X/UBL/CII)
    SPA->>AN: Lookup recipient by buyer SIREN/SIRET
    AN-->>SPA: Recipient PA address + routing code
    SPA->>RPA: Route structured invoice (Flux 1)
    RPA->>RC: Deliver invoice to recipient
    SPA->>DG: Report invoice data + lifecycle statuses
    RPA-->>SPA: Lifecycle status updates / CDAR
    SPA-->>SC: Status updates surfaced to customer

The five corners are: the sender (the Green-Got customer, represented by its SC), the sender’s PA (B2Brouter), the recipient’s PA, the recipient, and the PPF/DGFiP concentrator that receives reported data. The Annuaire lookup determines which recipient PA to route to.

6.1 Where B2Brouter and Green-Got sit

6.2 P2P same-PA shortcut

When both the sender and the recipient have chosen the same PA (for example, both are B2Brouter accounts), the invoice does not need to traverse a second platform. The PA delivers it internally (P2P), while still performing the Annuaire registration and reporting the data and statuses to the PPF concentrator. Both parties must still be Annuaire-registered for the routing to resolve.

sequenceDiagram
    autonumber
    participant SC as Green-Got (SC)
    participant PA as B2Brouter (single PA)
    participant RC as Recipient (same PA)
    participant DG as PPF concentrator → DGFiP

    SC->>PA: Submit invoice
    PA->>PA: Annuaire resolves recipient to same PA (P2P)
    PA->>RC: Internal delivery
    PA->>DG: Report invoice data + lifecycle statuses

7. Related Documents

8. Sources

Actors and Legal Posture

This document catalogues the parties in the French e-invoicing reform, their responsibilities and liabilities, and the mandate (mandat) model by which Green-Got and B2Brouter act on a customer’s behalf.

1. Terminology

The canonical glossary lives in 1. Reform Overview. Terms used most heavily here:

2. Actor Catalogue

ActorIdentity in the reformPrimary responsibilityLiability
Green-GotSolution Compatible (SC)Build the structured invoice, submit it to the PA, surface lifecycle statuses, run onboarding/mandate capture, generate payment linksSoftware correctness and data accuracy of what it submits; not a regulated intermediary
B2BrouterPlateforme Agréée (PA), Peppol Access PointFormat conversion, Annuaire lookup and routing, transmission to recipient PA, reporting to PPF/DGFiP, audit trail, integrity preservationSecure transmission, format compliance, audit trail; regulated as an approved platform
The customerassujetti / issuerProvide accurate invoice content, hold a valid mandat, declare its PA and routing scope in the Annuaire, retain archivesLegally responsible for invoice content and compliance and for archiving
The customer’s clientrecipient (buyer)Receive the invoice through its own PA, set business lifecycle statuses (accept / refuse)Responsible for its own reception and statuses
DGFiPTax administration / Peppol authorityRegister and audit approved platforms, run conformance testing, receive reported dataEnforcement and audit
PPFPublic portal (Annuaire + concentrator)Hold the directory, aggregate invoice data, statuses, and e-reporting for DGFiPState-run infrastructure

Design rule: Green-Got submits and surfaces; B2Brouter transmits and reports; the customer owns the content and the archive. Code and contracts must keep these boundaries explicit.

3. The Mandat Concept

A mandat is the legal authorization by which a company empowers an approved platform (PA) to act on its behalf — to issue invoices, receive invoices, perform e-reporting, and manage lifecycle statuses on its account.

Invariant: A mandat transfers operational authority, not legal responsibility. The company (the assujetti) remains legally responsible for the content and compliance of its invoices. The PA is responsible for secure transmission, format compliance, and the audit trail.

The split of duties:

See 9. Onboarding for how the mandate is captured operationally during PA registration.

4. Green-Got’s Legal Posture

Green-Got is a Solution Compatible. It is not an approved platform and carries none of the PA’s regulated transmission obligations.

What Green-Got is responsible for:

What Green-Got is NOT responsible for:

5. Switching PA

A company may change its approved platform. Switching has two operational consequences:

Design rule: Green-Got, as the SC, should not assume permanent exclusivity of B2Brouter for a customer. Onboarding, mandate, and data-export design must account for a future PA switch without breaking the customer’s Annuaire registration or audit trail. See 9. Onboarding.

Design rule — a rehearsed export package is a precondition for switching and for offboarding. The “data must be transferred” obligation above is satisfied by the launch-grade provider-exit export package defined in 8. Archiving §5.7 — legal artefacts + hashes, provider invoice ids, tax-report ids, lifecycle/CDAR status history, mandates (MandateEvidence), enrollment state, pending transmissions, request ids, and reconciliation state. That package must be tested (export → restore) before the first real customer is enrolled, so that a later PA switch or offboarding moves the full audit trail with no loss of legal evidence. The exact PA-switch handover format the final decree will require is still [open — legal/provider-support]; the export package is the launch-grade minimum Green-Got controls regardless.

Design rule — the mandate, not the subscription, is the legal basis for acting on a customer’s behalf. A live business-account subscription is a billing fact; it is not a legal authorization. The legal basis is the mandat, captured as audit-ready MandateEvidence (see 9. Onboarding §4.5). Two distinct things must not be conflated:

Losing inbound PA-designation (the customer’s Annuaire entry now points at another PA) does not revoke the issuing mandate. Outbound therefore continues while the issuing mandate is valid AND the subscription is active — subscription alone is never sufficient. See 9. Onboarding §5.5. The losing-PA continuity / data-transfer obligations described above are B2Brouter’s, as the PA, not the SC’s; their exact duration and format are unconfirmed pending the final decree.

Legal basis — losing-PA minimal-service window (PLF 2026 art. 28, decree-pending). The losing (old) PA’s obligation to maintain a minimal service during a transition window derives from PLF 2026 art. 28 (renumbered art. 123 in the adopted text), which extends the losing-platform minimal-service period from 6 to 12 months (amendment I-880 of 27 November 2025). During that window the old platform redirects residual flows that still arrive at the old routing address and keeps historical data accessible. The mechanism — including the exact modalities of the minimal service — must be specified by decree and is not yet final. This is the PA’s obligation (B2Brouter’s), not Green-Got’s as SC. [open — legal] (losing-PA minimal-service-window modalities, pending the final decree under PLF 2026 art. 28 / art. 123). Note that Green-Got’s own product choice for a customer who leaves it is unbounded continuity on the audit-trail/archive side (it never silently drops an enrollment — see 9. Onboarding §5.5), which exceeds the statutory floor and is independent of this PA-side window.

6. Liability Matrix

ConcernCustomer (assujetti)Green-Got (SC)B2Brouter (PA)DGFiP / PPF
Invoice content accuracy & VAT complianceOwnerFaithful submission of supplied dataAudit
Mandatory mentions presentOwnerValidation assistanceFormat validationAudit
Format compliance (EN 16931)Correct mapping to B2BrouterOwner (conversion/validation)Conformance testing
Routing & transmissionOwner
Reporting to PPF/DGFiP (Flux 1 / Flux 10)OwnerReceives
Reliable audit trail (PAF)Stays responsibleOwner (operational)Audit
Archiving & retentionOwner (legally)May archive on behalfAccess on request
Annuaire registration & routing scopeOwner (declares)Provisions via PARegisters in AnnuaireHosts Annuaire
Payment / settlementOwnerPayment links (own rails)None
Mandat validityGrants itCaptures itActs under it

7. Open Items

The following could not be confirmed from the cited research and are returned for the central uncertainties register (do not treat as settled here):

8. Related Documents

9. Sources

4. Formats and Invoice Data

Formats and Invoice Data

This document is the canonical structured-invoice field reference for Green-Got’s French e-invoicing: it defines the three mandated interoperability formats, the Factur-X anatomy, and the EN 16931 structured data model — including the mandatory mentions the reform adds and the code lists that qualify them.

1. Terminology

2. The socle minimum d’interopérabilité — exactly three formats

The reform does not mandate a single format. It mandates that every PA support an interoperable base — the socle minimum d’interopérabilité — of exactly three structured formats, all EN 16931-compliant. A French issuer may emit in any of the three; the recipient’s PA converts to the recipient’s preferred format while preserving integrity.

FormatKindSyntaxHuman-readableMachine-readableTypical use case
Factur-XHybridPDF/A-3 + embedded CII D22B XMLYes (the PDF)Yes (the XML)SME default; a printable invoice that is also fully structured. Lets staff read the same artefact the system parses.
UBL 2.1Pure XMLUBL (OASIS)NoYesERP/EDI integrations, Peppol exchanges, public-sector flows.
CIIPure XMLCII D22B (UN/CEFACT)NoYesEDI integrations; also the embedded syntax inside Factur-X. D22B is the reform syntax version (not the older D16B baseline).

Design rule: Green-Got’s canonical contract is the EN 16931 structured data model defined in this document, expressed as JSON to B2Brouter. B2Brouter generates the concrete Factur-X / UBL / CII artefact. Green-Got never hand-builds the XML for the JSON path; the data model here is the contract that those artefacts are validated against. See B2Brouter — Formats & validation.

Invariant: all three formats carry the same EN 16931 core semantics. A field that is mandatory in the model below is mandatory regardless of which of the three syntaxes is ultimately emitted.

2.1 Canonical syntax + profile matrix

The reform fixes a precise set of syntaxes and a precise set of profiles. Both axes are mandated by AFNOR XP Z12-012 (February 2026 version, in force — replaces the annulled November 2025 version; “Formats et Profils … du socle minimal applicable à la Réforme Facture Electronique”).

Syntaxes (the socle):

Profiles (the two reform profiles, both EN 16931-compliant):

Reform profileWhat it isBT-24 identifierWhen
EN16931French CIUS of EN 16931 (tightens, never extends the core)urn:cen.eu:en16931:2017Default for the reform perimeter; carries the full mandatory mention set with line detail.
EXTENDED-CTC-FRFrench EXTENSION of EN 16931 (a subset of Factur-X EXTENDED) adding reform terms — additional actors, multi-order/multi-delivery line references, contract type (EXT-FR-FE-01), line-level out-of-scope VAT (category O alongside other lines) — and adjusting some cardinalities/calc tolerancesurn:cen.eu:en16931:2017#conformant#urn:cpro.gouv.fr:1p0:extended-ctc-frWhen a case needs terms beyond the EN 16931 core (multi-order invoices, acompte reprises per line, mixed in/out-of-scope lines, additional transaction actors).

EN 16931 + EXTENDED-CTC-FR support matrix (which profile carries which capability):

CapabilityEN16931EXTENDED-CTC-FR
Full EN 16931 core (header, lines, VAT breakdown, totals, mandatory mentions)YesYes
Cadre de facturation B/S/M (BT-23) + structured legal notes (BT-21/BT-22)YesYes
Multi-order / multi-delivery line referencesNoYes
Per-line acompte reprise (reference to prior invoice at line level)NoYes
Additional actors (agent d’acheteur, payeur, agent de vendeur, adressé à, facturant)NoYes
Out-of-scope VAT (category O) lines mixed with other-category lines on one invoiceNo (BR-O-11 forbids)Yes (BR-O-11 relaxed)
±0,01 € per-line/charge rounding tolerance in foot-of-invoice sumsNoYes

Design rule: Green-Got targets the EN16931 profile by default and emits EXTENDED-CTC-FR only when a case requires a term the core cannot express (per the matrix). The Factur-X BASIC WL profile is the carrier AFNOR uses to describe the reform profiles’ “facture mixte sans données de lignes” structure; for domestic B2B Green-Got never emits MINIMUM or BASIC WL (they omit line detail and are not EN 16931-compliant on their own — see §3.2).

2.2 Schematron, fixture, and validation-gate policy

The reform’s code lists, Schematron, and EN 16931 rules evolve; the validation must always run against the currently published version. AFNOR XP Z12-012 §4.4.10 and §4.3.1 state the obligation explicitly: PAs and conformance-validation solutions “must use the latest published version of the validation tools”, and code lists are republished (with at least one month’s notice) twice a year (15 May / 15 November).

3. Factur-X anatomy

Factur-X is the format Green-Got emits by default for B2B, because a single file is both the legible invoice a human archives and reads, and the structured document the recipient’s system ingests.

3.1 Physical structure

flowchart TD
    PDF["PDF/A-3 container
(ISO 19005-3)"] Visual["Visual invoice page(s)
human-readable rendering"] XML["Embedded file: factur-x.xml
CII D22B syntax (UN/CEFACT)
EN 16931 semantics"] Meta["XMP metadata
declares the Factur-X profile + filename"] PDF --> Visual PDF --> XML PDF --> Meta

3.2 Profiles

Factur-X defines five profiles of increasing structured-data completeness. Higher profiles carry more EN 16931 business terms in the XML.

ProfileStructured contentEN 16931-compliantNotes
MINIMUMHeader + totals only; no line detailNoEffectively a structured cover sheet; insufficient for the French B2B mandate on its own.
BASIC WLHeader + totals, “without lines”NoDocument-level VAT breakdown but no line items.
BASICHeader + totals + line items (reduced)NoCarries line detail but is a Chorus Pro / French subset, not EN 16931-compliant per FNFE-MPE (only EN 16931 and EXTENDED are compliant — https://fnfe-mpe.org/factur-x/).
EN 16931 (comfort)Full EN 16931 core modelYesBaseline profile Green-Got targets — sufficient for most SME B2B invoices.
EXTENDEDEN 16931 + additional termsYes (superset)Cross-industry / complex cases beyond the core model.

Profile policy for French reform usage. The five Factur-X profiles map onto the reform’s two mandated profiles as follows. The distinction matters: a Factur-X file is only a valid reform e-invoice if it carries one of the two reform profiles (§2.1) with the full mandatory mention set.

Factur-X profileEN 16931-compliantValid for domestic B2B reform flows?Reason
MINIMUMNoNoHeader + totals only, no line detail; cannot carry the mandatory line-level mentions. AFNOR uses it/BASIC WL only to describe structure, not as a compliant emission.
BASIC WLNoNo“Without lines” — document-level VAT breakdown but no line items; insufficient for the mandate. (It is the profile AFNOR uses to describe the mixed-invoice structure of the reform profiles, not a profile Green-Got emits.)
BASICNoNoA Chorus Pro / French subset that carries line detail but is not EN 16931-compliant per FNFE-MPE (the authoritative maintainer); only EN 16931 and EXTENDED are EN 16931-compliant and reform-valid (https://fnfe-mpe.org/factur-x/). Green-Got never emits it for domestic B2B reform flows.
EN 16931 (comfort)YesYes — defaultLowest profile carrying the full mandatory mention set (§4) with line detail. Maps to the reform EN16931 profile.
EXTENDEDYes (superset)Yes — when neededSuperset of the core; the reform EXTENDED-CTC-FR profile is a France-specific subset of it. Used only when a case needs terms beyond the EN 16931 core (§2.1).

Design rule: Only the EN 16931 and EXTENDED Factur-X profiles are EN 16931-compliant and therefore reform-valid (per FNFE-MPE — https://fnfe-mpe.org/factur-x/). Green-Got’s baseline Factur-X profile is EN 16931 (mapping to the reform EN16931 profile). It is the lowest profile that carries the full mandatory mention set defined in §4 with line-level detail. EXTENDED (reform EXTENDED-CTC-FR) is used only when a case requires terms beyond the EN 16931 core; MINIMUM, BASIC WL, and BASIC are never emitted for domestic B2B reform flows because they are not EN 16931-compliant.

3.3 Profile selection algorithm (EN16931 vs EXTENDED-CTC-FR)

The profile is derived in the pre-submission validation layer (§7), never hand-picked. The default is EN16931; the invoice is escalated to EXTENDED-CTC-FR if and only if it carries a term the EN 16931 core cannot express. Evaluate in order:

  1. Start with target profile = EN16931.
  2. Escalate to EXTENDED-CTC-FR if any of the following holds (the EXTENDED-only capabilities of the §2.1 support matrix):
    • any line references more than one order or delivery (multi-order / multi-delivery line references);
    • any line carries a per-line reprise d’acompte reference to a prior invoice (line-level prior-invoice reference);
    • the invoice carries additional transaction actors beyond seller/buyer/payee (agent d’acheteur, payeur, agent de vendeur, adressé à, facturant);
    • a category O (out-of-scope) line coexists with lines of other VAT categories on the same invoice (forbidden under EN16931 by BR-O-11; permitted only under the relaxed EXTENDED-CTC-FR — see §5);
    • a per-line/charge rounding of ±0,01 € is required in the foot-of-invoice sums (tolerance available only in EXTENDED-CTC-FR);
    • contract type (EXT-FR-FE-01) or any other EXTENDED-only reform term is present.
  3. Otherwise emit EN16931.

The decision is computed before submission and the resulting BT-24 identifier (§2.1) is declared accordingly. B2Brouter’s Schematron is the second line of defence, not the selector.

4. Mandatory mentions added by the reform

On top of the pre-existing French VAT-invoice mention rules (CGI art. 242 nonies A), the e-invoicing reform adds and reinforces a set of mandatory mentions, principally to make every invoice machine-routable and machine-auditable. The table below is the canonical field list. Each row maps to an EN 16931 business term where one exists.

FieldEN 16931 termMandatory?Notes
Seller SIRENBT-30 (seller legal registration id, scheme 0002)MandatoryThe issuer’s 9-digit legal-entity id. BT-30 is the legal registration identifier and carries the SIREN (scheme 0002).
Seller SIRETBT-29 (seller identifier, scheme 0009)ConditionalEstablishment-level id (SIREN + NIC) when invoicing from a specific establishment. The SIRET is carried on BT-29 (seller identifier), distinct from BT-30 (legal registration = SIREN). The exact BT-29/BT-30 ↔ SIREN/SIRET split is pinned against AFNOR XP Z12-012 (February 2026) at implementation and used consistently across this doc and §9.1.
Seller name, addressBG-4MandatoryPre-existing VAT-invoice requirement, retained.
Seller VAT numberBT-31ConditionalMandatory when the issuer has/uses a VAT identifier or when required by the operation type; absent or exemption-coded for franchise-en-base / legally-exempt issuers that have no VAT number. SIREN/SIRET (not the VAT number) is the always-mandatory identifier for routing and legal identity.
Buyer SIRENBT-47 (buyer legal registration id, scheme 0002)Conditional — domestic FR B2B / Flux 1 onlyMandatory only for a domestic French B2B buyer (a FrenchCompany / PublicEntity routed via Flux 1), where it is the identification + Annuaire routing key. It is not present (or required) for B2C (Individual), ForeignCompany, or other Flux 10 e-reporting cases (CGI art. 290), which carry no French buyer SIREN. Validators must branch by party type / channel — never demand a SIREN globally, and never invent a French identifier to satisfy the field. See 6. Annuaire and Routing and invoicing/11. API Contract.
Buyer SIRETBT-47 variantConditionalWhen the buyer designates a specific establishment for receipt/routing.
Buyer name, addressBG-7 (buyer group): name BT-44, postal address BG-8MandatoryPre-existing requirement, retained. The buyer party block is BG-7; within it the buyer name is BT-44 and the postal address is BG-8 (BT-50…BT-55). Used consistently with §9.2.
Buyer VAT numberBT-48ConditionalRequired only when legally applicable — intra-EU supply, reverse charge (autoliquidation), or where the buyer’s VAT identification drives the operation’s tax treatment. Not universally mandatory: for ordinary domestic B2B the central buyer identifier is the SIREN/SIRET, which is what routes and identifies the invoice.
Delivery address (when different from billing)BG-15 (BT-75…BT-80)MandatoryDistinct delivery / “lieu de livraison” address must be carried when it differs from the billing address.
Goods-vs-services composition — cadre de facturationBT-23 (B / S / M)MandatoryThe invoice must indicate, via the cadre de facturation BT-23, whether it covers Biens (goods), Services, or Mixte (independent goods and services lines) — rule BR-FR-08. This is a structured code, not free text. The composition is grounded in the line-level VAT category/chargeability (§5); the BT-23 cadre is the document-level summary derived from the lines.
VAT chargeability — debits vs encaissementline-level category context → document-level derived statementMandatory (when applicable)VAT chargeability is qualified per line (a goods line is on débits; a services line is on encaissement unless the provider opted for débits). The document-level mention — including the explicit “option pour le paiement de la taxe d’après les débits” when a service provider elected it — is derived from the lines, not stored independently. Goods do not elect collection; only service providers opt for débits (§1, §4.1 below).
Operation category (type of transaction for VAT)tax category + scheme contextMandatoryThe nature of the operation for VAT purposes (standard domestic supply, reverse charge, intra-EU, export, exempt, out of scope). Expressed via the VAT category code, see §5.
Document typeBT-3 (UNCL1001)MandatoryInvoice / credit note / prepayment etc. See §6.
Invoice numberBT-1MandatoryUnique, sequential, gap-free per issuer numbering scheme.
Invoice issue dateBT-2Mandatory
Payment termsBT-20 / BT-9 (due date)MandatorySettlement terms and due date.
Payment meansBG-16 (BT-81 means code, BT-84 IBAN…)MandatoryHow payment is to be made (IBAN, transfer, etc.). Metadata only — B2Brouter and the PA settle nothing. See B2Brouter — Payment.
Line item: descriptionBT-153MandatoryPer line of invoice_lines.
Line item: quantityBT-129Mandatory
Line item: unit price (net)BT-146Mandatory
Line item: line net amountBT-131MandatoryQuantity × unit price, net of VAT.
Line item: VAT category + rateBT-151 / BT-152MandatoryPer line. Category from UNCL5305; rate as a percentage.
Document totals: total netBT-109 (sum of lines net)Mandatory
Document totals: VAT breakdown per rateBG-23 (BT-116…BT-119)MandatoryTaxable base and VAT amount per category/rate.
Document totals: total VATBT-110Mandatory
Document totals: total gross (grand total)BT-112MandatoryNet + VAT; amount payable.
CurrencyBT-5MandatoryISO 4217. Domestic French B2B is EUR; cross-border may differ.

Invariant: for domestic FR B2B (Flux 1) the buyer SIREN is the routing key — a domestic-B2B invoice that lacks a resolvable buyer SIREN cannot be routed through the Annuaire and is not a valid domestic B2B e-invoice. This invariant is scoped to the domestic FR B2B channel; it does not apply to B2C (Individual), ForeignCompany, or other Flux 10 e-reporting operations, which have no French buyer SIREN and are validated on their own party-type/channel rules. The canonical party model splits FrenchCompany, ForeignCompany, Individual, and PublicEntity, and validators branch by channel — see 6. Annuaire and Routing and invoicing/11. API Contract.

Design rule: Green-Got’s internal invoice model (see invoicing crate docs) must carry every Mandatory row above as a first-class field, not a free-text mention. The reform’s purpose is structured data; free-text rendering of a mandatory field is non-compliant if the corresponding structured term is absent.

4.1 VAT chargeability: line-level stored, document-level derived

Decision. VAT regime and chargeability are stored at line level and the document-level flags/statements are derived from the lines. This mirrors the EN 16931 / AFNOR model: AFNOR XP Z12-012 §4.4.7 qualifies VAT “pour chaque ligne de facture”, and §4.4.5 derives the foot-of-invoice VAT breakdown (“chaque catégorie de TVA présente dans les lignes doit être présente dans la ventilation de TVA en pied”). It is also already the chosen Green-Got model (line-level VAT). This document does not re-open that decision; it states the consequences for the mandatory mentions.

Mixed goods/services behaviour. When an invoice carries both goods and services lines:

Correction to a common misstatement. “Goods are always on débits” is over-broad: it holds only for taxable seller-collected goods (S/Z). A domestic reverse-charge goods line (category AE) — a real AR case under CGI art. 283-2 sexies (déchets neufs d’industrie / matières de récupération) and art. 283-2 nonies (sous-traitance bâtiment), per BOFiP (https://bofip.impots.gouv.fr/bofip/3218-PGP.html) — collects no seller VAT and carries no chargeability basis; it carries the reverse-charge reason instead. Equally, services cannot be forced onto débits and goods cannot “elect VAT on collection”: for taxable supplies, goods are chargeable on delivery (débits) while services are on encaissement by default with a one-way provider opt for débits. Any model or mention that assigns a chargeability basis to an AE/K/G/E/O line, or that lets a goods line elect collection, is wrong and must not be emitted.

4.2 Structured legal mentions (BT-21 / BT-22 notes)

Several mandatory or conditionally-mandatory French legal mentions have no dedicated business term in the EN 16931 core. The reform requires them as structured data, not free text: they are carried as a note (text in BT-22) qualified by a subject code in BT-21 (UNCL4451), per AFNOR XP Z12-012 §4.4.3 (rules BR-FR-05 / BR-FR-06 / BR-FR-07). Green-Got models each as a first-class structured legal-note field — a (subject_code, text) pair — not a free-text blob.

Note on BT-21 validity. The constraint that BT-21 carries one of the reform-prescribed subject codes is an AFNOR BR-FR national rule (§4.4.3 / BR-FR-05/06/07), not an EN 16931 BR-CL-* Schematron code-list rule (e.g. BR-CL-08, which governs other code lists). Validate BT-21 subject values against the AFNOR annex + the live UNCL4451 release, not against an EN16931 BR-CL rule.

The (mention → BT-21 code) pairs below must be pinned against the AFNOR XP Z12-012 §4.4.3 annex + the live UNCL4451 release before implementation — the UNCL4451 standard titles are generic and differ from the French legal mention names (see §1). The “UNCL4451 standard title” column quotes the verbatim UN/EDIFACT D96A title (https://service.unece.org/trade/untdid/d96a/uncl/uncl4451.htm); the French legal mention is mapped onto it via the AFNOR national convention.

Legal mentionBT-21 code (to pin vs AFNOR annex)UNCL4451 standard titleTypical contentWhen
Recovery fee — indemnité forfaitaire de recouvrementPMT“Payment information”“Indemnité forfaitaire pour frais de recouvrement : 40 €”Mandatory on B2B invoices (CGI / Code de commerce L441-10).
Late-payment penalties — pénalités de retardPMD“Payment detail/remittance information”Penalty rate / terms applicable on late paymentMandatory on B2B invoices.
Discount terms — escompteAAB“Terms of payments”“Escompte pour paiement anticipé : …” or “Pas d’escompte pour paiement anticipé”Mandatory (state the discount terms, or that none apply).
Legal information — mentions légalesABL“Government information”RCS / N° registre des métiers, capital social, etc.Pre-existing legal-footer mentions.

Code-list caveat. BLU (éco-contribution DEEE) and BAR (routing indication) cited in earlier drafts are not valid UNCL4451 codes (verified absent from D96A). Do not assert them. If an éco-contribution DEEE mention or a routing-indication mention must be carried as a structured note, re-derive the correct UNCL4451 subject code (or AFNOR-registered code) from the AFNOR annex first. The titles above are the generic standard subjects, not the French legal labels.

Organization-level defaults and legal_notes[] storage. The recovery-fee (PMT), late-penalty (PMD), and discount (AAB) notes are usually identical across an organization’s invoices. Green-Got stores them as organization-level defaults (set once on the issuer’s billing profile). Each invoice carries a legal_notes[] array of (subject_code, text) pairs that is snapshotted at finalize:

Same source renders to PDF and XML. These structured legal-note fields are the single source for both faces of a Factur-X invoice: the visual PDF/A-3 renders them as the human-readable legal footer, and the embedded XML emits the same (BT-21, BT-22) pairs. There is no separate “footer text” — the PDF mentions and the XML notes are projections of the same structured fields, which is what keeps the two faces consistent (§3.1). Ownership: Green-Got supplies the structured (BT-21, BT-22) pairs with full data fidelity; B2Brouter (the PA) renders the visual PDF footer and emits the XML notes from those pairs at generation. Green-Got does not hand-render the footer — it owns the data, not the layout.

5. VAT category codes (UNCL5305)

The operation category for VAT is expressed through the EN 16931 VAT category code (UNCL5305, BT-151 at line level, BT-118 at breakdown level). Green-Got carries the code explicitly; B2Brouter maps it into the emitted XML.

CodeMeaningTypical use
SStandard rateOrdinary taxable domestic supply at the standard or a reduced positive rate.
ZZero ratedTaxable at 0%.
EExemptExempt from VAT (with statutory exemption reference).
AEVAT Reverse ChargeBuyer accounts for VAT (autoliquidation).
KVAT exempt for intra-community supply of goodsIntra-EU supply of goods to a VAT-registered buyer.
GFree export item, tax not chargedExport of goods outside the EU.
OServices outside scope of taxOperation out of the scope of VAT.

Version pin. UNCL5305 and the EN 16931 / BR-* rule set evolve; AFNOR republishes the code lists twice a year (15 May / 15 November, §2.2). The category list and the BR-* rules below must be validated against the release current at submission time — they are not pinned into the domain. Citations here are against Peppol BIS Billing 3.0 (EN 16931) — https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/ and https://docs.peppol.eu/poacc/billing/3.0/rules/ubl-tc434/.

Per-category rate rules (EN 16931 BR-* family, verified against Peppol BIS 3.0):

CodeRate ruleExemption/reverse-charge reason carrier
Srate > 0 (BR-S-05)none
Zrate = 0 exactly (BR-Z-05)none
Erate 0BT-120 text and/or BT-121 VATEX code (BR-E-10)
AErate 0BT-120 text and/or BT-121 VATEX code — VATEX-EU-AE for reverse charge (BR-AE-10)
Krate 0BT-120 text and/or BT-121 VATEX code (BR-IC-10)
Grate 0BT-120 text and/or BT-121 VATEX code (BR-G-10)
Ono rate / out of scopeBT-120 text and/or BT-121 VATEX code (BR-O-10)

The earlier blanket claims — “S and Z carry a non-negative rate” and “exemption reason is Mandatory for all of E/AE” — were wrong: Z is exactly 0 (not merely non-negative), S is strictly > 0, and the exemption-reason obligation is per-category (BR-AE/E/G/IC/O-10), carried as BT-120 text and/or the BT-121 VATEX code, not a single blanket rule.

5.1 Permissible VAT-category combination matrix

§7 declares this document the primary validation contract (Schematron is the second line of defence), so the permissible combinations of categories on one invoice must be encoded here and validated before submission, derived from the EN 16931 BR-* family:

First category → / Other category ↓SZEAEKGO
S✗ (AE all-or-nothing)✗ (BR-O-11) / EXT only
Z✗ / EXT only
E✗ / EXT only
AE✗ / EXT only
O✗ / EXT only✗ / EXT only✗ / EXT only✗ / EXT only✗ / EXT only✗ / EXT only

“✗ / EXT only” = forbidden under EN16931, permitted only under EXTENDED-CTC-FR. The exact pairing for less common combinations (E/K/G with each other) must be re-derived against the live BR-* release at submission time; the matrix above pins the load-bearing rules (BR-O-11; AE all-or-nothing).

6. Document type codes (UNCL1001)

The document type (BT-3) classifies the document. Green-Got sets it explicitly per document; B2Brouter exposes it as type_document on the invoice object.

CodeMeaning (FR)Description
380Facture commercialeCommercial invoice — the default document type.
381Avoir / note de créditCredit note — the corrective instrument for a finalized invoice (gap-free, immutable invoices are never deleted; corrections issue a 381). 381 is the standard French avoir. Code 261 is a distinct UNCL1001 type — a self-billed credit note (the credit-note mirror of self-billed invoice 389) — and is not the standard avoir; Green-Got uses 381 for ordinary credit notes and reserves 261 for the self-billed case.
384Facture rectificativeCorrected invoice — replaces/rectifies a previously issued invoice. Enumerated by AFNOR XP Z12-012 §4.1 (380 = Facture Commerciale, 381 = Avoir, 384 = Facture Rectificative).
386Facture d’acomptePrepayment / advance-payment invoice (acompte).
389Facture auto-facturéeSelf-billed invoice (issued by the buyer on behalf of the seller).

Allowed set is enumerated — no arbitrary codes. The invoice type code (BT-3) is drawn from the UNCL1001 code list, and the reform’s allowed values are an enumeration maintained by the AFNOR code lists (§4.1, §4.4.10), republished twice a year. Green-Got selects from the enumerated set above and does not invent codes. In particular there is no 326 “facture partielle” in the reform set: progress / partial / acompte billing is modelled through the allowed type codes and the cadre/mode de facturation, not a new code:

Preceding-invoice reference (BG-3 / BT-25 / BT-26). The link from a 381 (avoir) or 384 (facture rectificative) back to the invoice(s) it corrects is carried by the EN 16931 “Preceding invoice reference” group BG-3BT-25 (preceding invoice number) and BT-26 (preceding invoice issue date) — not BT-3 (BT-3 is the document type code of the current document). BG-3 is modelled as 0..n: a single avoir/rectificative may reference multiple preceding invoices. 381-vs-384 reconciliation: a 381 (avoir) reverses the referenced invoice(s) (a separate negative-amount document that nets against them, both remaining in the ledger), whereas a 384 (facture rectificative) supersedes the referenced invoice (the rectificative is the operative document for the corrected operation). Both carry their BG-3 references to the original; the choice between them is a domain decision (reverse vs supersede), not a syntax concern.

Design rule: a finalized, gap-free invoice (type 380) is immutable and is never modified or deleted. Any correction is a separate 381 (avoir) or 384 (facture rectificative) that references the original. This is a hard requirement of gap-free numbering and the audit trail, not a soft convention.

Design rule — the internal model is a superset of the allowed values. Green-Got’s canonical structured-invoice model holds every field any required target needs: all EN 16931 BT/BG business terms (§4), the enumerated UNCL1001 document types (380 / 381 / 384 / 386 / 389), all UNCL5305 VAT categories (§5), and all DGFiP-required mentions. “Superset” means it can express every field and every allowed code — it does not license inventing codes outside the AFNOR enumeration. Producing Factur-X / UBL / CII / Chorus Pro / a future format, or satisfying a DGFiP obligation, is therefore a mapper concern, not a domain change: a mapper projects the rich internal model onto each external representation. Adding a field, a document type, a target format, or an error code is a mapper change. The exact EN 16931 BT/BG numbers and the Factur-X profile to select are validated by the format mapper / the PA (B2Brouter) at generation time (XSD + Schematron, see §7), not hardcoded into the domain. This is how Green-Got holds the full DGFiP / EN 16931 field set without coupling the domain to any one syntax: one rich canonical model in, many projections out.

7. From JSON to XML — the generation boundary

Green-Got submits the structured data above as JSON to B2Brouter; B2Brouter generates the concrete Factur-X / UBL / CII artefact, validates it (XSD syntax + EN 16931 Schematron business rules), and routes it. See B2Brouter — Sending invoices and B2Brouter — Formats & validation.

Design rule: the XML B2Brouter generates is a projection of the data model in this document; this document is the source of truth for which fields exist, which are mandatory, and what they mean. When the B2Brouter JSON field names differ from the EN 16931 term names, the mapping is documented in the B2Brouter docs — but a field’s mandatory/optional status is governed here, not by B2Brouter’s API surface.

Invariant: Green-Got validates its own invoice model against this document before submission. B2Brouter’s Schematron validation is a second line of defence, not the primary contract.

8. Related documents

9. Field Correspondence (canonical ↔ external representations)

This is the complete field correspondence table: every field Green-Got’s canonical internal invoice model carries, mapped across the representations it is projected onto. It is the field half of the format-transposition mapping; the status half is 5. Lifecycle Statuses §10 + §9. The columns are:

9.1 Seller

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Seller legal nameBT-27Seller name(via B2Brouter generation — sender = account)Issuer nameMandatorySender is the customer’s own B2Brouter account; seller block derives from account/KYB, not per-invoice JSON.
Seller SIRENBT-30 (scheme 0002)Seller legal registration id(via B2Brouter generation — account cin_value, scheme 0002)Issuer SIRENMandatory9-digit legal-entity id; sourced from the account CIN (cin_scheme 0002), not the TIN (which carries the VAT number).
Seller SIRETBT-29 (scheme 0009)Seller id (SIRET)(via B2Brouter generation)Issuer SIRETConditionalEstablishment-level (SIREN + NIC) carried on BT-29 (seller identifier), distinct from BT-30 (legal registration = SIREN). BT-29/BT-30 split pinned against AFNOR Feb-2026 (§4).
Seller VAT numberBT-31Seller VAT identifier(via B2Brouter generation — account tin_value, tin_scheme 9957)Issuer VAT no.ConditionalFrench intra-community VAT number. Mandatory when the issuer has/uses a VAT identifier or when the operation type requires it; absent or exemption-coded for franchise-en-base / legally-exempt issuers. Routing identity is the SIREN/SIRET (account CIN), not the VAT number. See §4.
Seller address (lines/zip/city/country)BG-5 (BT-35…BT-40)Seller postal address(via B2Brouter generation — account address/city/postalcode/country)Issuer addressMandatoryRegistered address, sourced from the account/KYB.

9.2 Buyer

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Buyer legal nameBT-44Buyer namecontact_id → contact nameCustomer nameMandatoryBuyer is a B2Brouter contact; contact_id references it.
Buyer SIRENBT-47 (scheme 0002)Buyer legal registration idcontact_id → contact id (SIREN)Customer SIRENConditional — domestic FR B2B / Flux 1The routing key for domestic FR B2B: a domestic-B2B invoice without a resolvable buyer SIREN cannot be routed via the Annuaire. Not required for B2C (Individual), ForeignCompany, or Flux 10 e-reporting cases, which carry no French buyer SIREN — validators branch by party type / channel (6. Annuaire and Routing).
Buyer SIRETBT-47 variantBuyer id (SIRET)contact_id → contact establishmentCustomer SIRETConditionalWhen the buyer designates a specific establishment for receipt/routing.
Buyer VAT numberBT-48Buyer VAT identifiercontact_id → contact VATCustomer VAT no.ConditionalRequired for intra-EU / reverse-charge operations.
Buyer billing addressBG-8 (BT-50…BT-55)Buyer postal addresscontact_id → contact addressCustomer addressMandatoryThe “adresse de facturation”.
Buyer delivery addressBG-15 (BT-75…BT-80)Deliver-to / “lieu de livraison”delivery address fields (via B2Brouter generation)Delivery addressMandatory when ≠ billingDistinct delivery address must be carried when it differs from billing (§4).
Buyer referenceBT-10Buyer referencebuyer_referenceBuyer referenceConditional (often required B2G)Cost-centre / Chorus “code service”; mandatory toward public buyers.

9.3 Invoice header

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Invoice numberBT-1Invoice numbernumberInvoice no.MandatoryUnique, sequential, gap-free.
Issue dateBT-2Issue datedateIssue dateMandatory
Due dateBT-9Payment due datedue_dateDue dateMandatorySettlement deadline (§4).
Service/delivery dateBT-72 (BG-14 period)Actual delivery date / invoicing period(via B2Brouter generation)Service dateConditional“Date de livraison/prestation” when it differs from the issue date.
CurrencyBT-5Document currency codecurrencyCurrencyMandatoryISO 4217; EUR for domestic French B2B.
Document type codeBT-3 (UNCL1001)Invoice type codetype_documentDocument typeMandatoryEnumerated set 380/381/384/386/389 — see §6.
Operation category (goods/services + VAT nature)line/document VAT category context (UNCL5305)TypeCode / tax category context(via per-line taxes_attributes category)(drives VAT display)MandatoryGoods-vs-services composition + VAT nature; expressed through the VAT category code (§5).
VAT-on-debits / encaissement optiondocument-level statementPayment terms / tax notepayment_terms (AAB) / (via B2Brouter generation)VAT regime mentionMandatory (when applicable)The “option pour le paiement de la taxe d’après les débits” for service providers (§4). Carried in the B2Brouter France native payment_terms field (the AAB / due-date / penalties / discount-conditions carrier), not the generic terms field.
Order / PO referenceBT-13 (purchase order) / BT-14 (sales order)Order referenceponumber / order_numberPO / order refConditionalponumber = buyer PO (BT-13); order_number = seller’s order id (BT-14).

9.4 Lines (invoice_lines_attributes)

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Line groupBG-25Invoice lineinvoice_lines_attributes[]Line rowMandatoryEach element is one invoice line.
Line descriptionBT-153Item name/descriptioninvoice_lines_attributes[].descriptionDescriptionMandatory
Line quantityBT-129Invoiced quantityinvoice_lines_attributes[].quantityQtyMandatory
Line unit price (HT)BT-146Item net priceinvoice_lines_attributes[].priceUnit priceMandatoryNet of VAT.
Line net totalBT-131Line net amountinvoice_lines_attributes[].* (qty × price, via generation)Line total HTMandatory
Line VAT category codeBT-151 (UNCL5305)Line tax categoryinvoice_lines_attributes[].taxes_attributes[].category(VAT badge)MandatoryS/Z/E/AE/K/G/O — see §5.
Line VAT rateBT-152Line tax rateinvoice_lines_attributes[].taxes_attributes[].percentVAT %MandatoryPercentage; 0 for AE/K/G/E/O.
Line tax name(tax label)Tax scheme nameinvoice_lines_attributes[].taxes_attributes[].name(tax label)ConditionalB2Brouter taxes_attributes.name (e.g. “VAT”/“TVA”).

9.5 VAT breakdown (per category)

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
VAT breakdown groupBG-23VAT breakdown (per category)(via B2Brouter generation — aggregated from line taxes_attributes)VAT summary rowsMandatoryOne row per category/rate.
Taxable base per categoryBT-116Category taxable amount(via B2Brouter generation)Base HT (per rate)Mandatory
VAT category codeBT-118 (UNCL5305)Category code(via line taxes_attributes[].category)(per-rate category)Mandatory
VAT rate per categoryBT-119Category rate(via line taxes_attributes[].percent)RateMandatory
VAT amount per categoryBT-117Category tax amount(via B2Brouter generation)VAT (per rate)Mandatory
Exemption / reverse-charge reasonBT-120 (text) / BT-121 (VATEX code)VAT exemption reason text/code(via B2Brouter generation)Exemption mentionPer-category (BR-AE/E/G/IC/O-10)Each reason-carrying category requires BT-120 text and/or the BT-121 VATEX code under its own rule (BR-AE-10/BR-E-10/BR-G-10/BR-IC-10/BR-O-10) — VATEX-EU-AE for reverse charge AE. Not a single blanket “mandatory” rule (§5).

9.6 Totals

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Total net (HT)BT-106 / BT-109 (tax exclusive)Sum of line net / tax-exclusive amount(via B2Brouter generation)Total HTMandatoryBT-106 = sum of line net; BT-109 = tax-exclusive total.
Total VATBT-110Total VAT amount(via B2Brouter generation)Total VATMandatory
Total gross (TTC)BT-112Tax-inclusive amount(via B2Brouter generation)Total TTCMandatoryNet + VAT; grand total.
Already-paid / prepaidBT-113Paid amount(via B2Brouter generation)PrepaidConditionalSums of prepayments already settled.
Amount dueBT-115Amount due for payment(via B2Brouter generation)Amount dueMandatoryTTC − already-paid.

9.7 Payment means

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Payment means groupBG-16Payment instructions(payment fields below)Payment blockMandatoryMetadata only — B2Brouter/the PA settle nothing (§4, 5. Lifecycle Statuses §3.4).
Payment methodBT-81 (means type code)Payment means type codepayment_methodPayment methodMandatory
Payment method text (PMT)note PMT (BT-21/BT-22)Payment means text / recovery-fee notepayment_method_textPayment method textMandatory (native)B2Brouter France IssuedInvoice native field; carries the PMT mention (indemnité forfaitaire de 40 € pour frais de recouvrement). Missing it → HTTP 422. See §4.2.
Creditor IBANBT-84Payment account id (IBAN)ibanIBANConditional (mandatory for transfer)
Remittance information (PMD)BT-83 / note PMD (BT-21/BT-22)Remittance information / late-penalty noteremittance_informationReferenceMandatory (native)B2Brouter France IssuedInvoice native field; carries the PMD mention (pénalités de retard). Missing it → HTTP 422.
Creditor referenceBT-90Creditor reference (SEPA)creditor_referenceCreditor refConditional
Payment terms (AAB / due date / penalties / discount)BT-20 / BT-9Payment termspayment_termsPayment termsMandatory (native)B2Brouter France IssuedInvoice native field; carries settlement terms / due date / late-payment penalties / discount conditions (AAB escompte). Missing it → HTTP 422. Also carries the VAT-on-debits option statement (§9.3). The generic terms field is a to-confirm legacy/generic alias[open — provider-support] whether it remains accepted alongside payment_terms for France B2B; the current France B2B field is payment_terms.

9.8 Attachments / original document

Canonical internal fieldEN 16931 term (BT/BG)Syntax (Factur-X/UBL/CII)B2Brouter JSON fieldBusiness-app (display)Mandatory?Notes
Supporting attachmentBG-24 (BT-122 ref, BT-125 binary)Additional supporting document(via B2Brouter generation / attachment upload)AttachmentConditionalAnnexes; for Factur-X the visual PDF/A-3 is the carrier (§3).
Preceding invoice referenceBG-3 (BT-25 number, BT-26 issue date)Preceding invoice reference(via B2Brouter generation)Corrected invoice refConditionalA 381 (avoir) or 384 (rectificative) references the preceding invoice(s) via BG-3 / BT-25 / BT-26not BT-3 (which is the current document’s type code). Modelled 0..n (a credit note / rectificative may reference multiple invoices) — see §6.

Invariant — BT/BG numbers AND code-list values are validated against the official spec at implementation. The EN 16931 BT/BG numbers in the table above are the conventional codes for orientation. The authoritative business-term number, cardinality, and the Factur-X profile that carries each field are validated against the official EN 16931-1:2017 / FNFE-MPE specification at implementation time, inside the format mapper — never hardcoded into the domain. The same hedge extends to code-list values — the UNCL5305 VAT categories (§5), UNCL1001 document types (§6), UNCL4451 note subjects (§4.2), and VATEX exemption codes — whose membership and titles must be re-validated against the live release at submission time, not just the BT/BG numbers. Sources: EN 16931-1:2017 semantic model and FNFE-MPE (https://fnfe-mpe.org/factur-x/); Peppol BIS Billing 3.0 BR-* / code lists (https://docs.peppol.eu/poacc/billing/3.0/). This is the same “internal model is a superset” rule as §6: the domain carries the rich field; the mapper owns the exact syntactic projection and its spec-conformant codes.

Design rule — this table is the field-mapper specification. This table is the spec the field mappers implement: one canonical internal model in, a projection per external representation out (Factur-X / UBL / CII via B2Brouter, the B2Brouter JSON contract, the business-app display). Adding a field or a new target format means extending this table and the corresponding mapper — it is a mapper change, never a domain restructuring, and never a per-syntax fork of the model. Conversely, a field’s mandatory/optional status is governed by §4 and this table, not by any one external API’s surface. Together with the status correspondence in 5. Lifecycle Statuses §10 + §9, this is the complete specification for transposing an invoice between any two supported representations.

10. Sources

5. Lifecycle Statuses

Lifecycle Statuses

This document is the canonical, authoritative model for the invoice lifecycle status across the three distinct layers Green-Got operates in — the AFNOR XP Z12-012 legal statuses, the B2Brouter API statuses, and Green-Got’s internal statuses — and defines the exact mapping between them in both directions. It is the cross-referenced source of truth that every other invoicing, bills, and Plateforme Agréée doc cites when it talks about “status”.

Terminology

1. Overview — Three Distinct Status Layers

Status in this system is not one enum — modelling it as a single combined LifecycleStatus is incorrect. There are three separate layers, each with its own vocabulary, owner, and purpose:

LayerVocabularyOwner / source of truthPurpose
1. AFNOR XP Z12-012 legal statusesDéposée, Rejetée, Refusée, Encaissée, … (French)The regulation; transmitted to and reconciled by the PPF concentratorThe legal lifecycle record. What DGFiP sees.
2. B2Brouter API statusesnew, sending, sent, accepted, registered, … (English)B2Brouter (the PA), via REST API + webhooksThe PA’s operational state of the transmission. How Green-Got learns what happened.
3. Green-Got internal statusesDraft, Issued, Delivered, Settled, … (projection)Green-Got (this codebase)What Green-Got persists and shows users. A projection.

Design rule: The AFNOR legal status is the source of truth for the regulatory lifecycle. The B2Brouter API status is the source of truth for the operational transmission state (it is how the legal status actually arrives). The Green-Got internal status is a projection, never the legal record — it is derived from the other two and must never be treated as authoritative for compliance.

Design rule: The four mandatory AFNOR statuses (Déposée, Rejetée, Refusée, Encaissée) must always be derivable from what Green-Got persists. Green-Got stores the raw B2Brouter API status and CDAR events on the transmission so the mandatory legal status can be reconstructed at any time, even if the internal projection collapses several legal states into one user-facing label.

Design rule: Direction matters. The OUTBOUND (issued) status flow and the INBOUND (received) status flow use different B2Brouter status vocabularies and map to different internal status sets in different crates (invoicing vs bills). They MUST be modelled separately. See §5 and §6.

1.1 Transport-agnostic internal model

Design rule — one canonical internal model, mappers to every external representation. Green-Got’s internal model is built around the AFNOR legal status (Layer 1) and carries the full AFNOR 14-status subtlety plus the raw transport evidence — it is the single source of truth for Green-Got’s own representation of every status (the canonical model the domain reads/writes). This does not contradict §1’s rule that the internal status is “a projection, never the legal record”: the two sentences are about different facts. The legal lifecycle fact is sourced from B2Brouter/PPF and is only projected into the internal model; the canonical internal model itself (the shape every mapper bridges to) is the single authoritative representation Green-Got owns. Source of truth is defined by fact in §9.1 and in the status support matrix at 10. Integration Contracts §13. Everything else is reached through a mapper: the domain never speaks another system’s vocabulary directly. There are three mapping targets:

MapperMaps the internal model to/from…Purpose
AFNOR mapperthe AFNOR legal status (codes 200–213)the regulatory record reported/derived per the standard
PA / B2Brouter mapperthe PA’s API status + CDAR payloadwhat the transport (B2Brouter today, another PA tomorrow) understands
Business-app mapperthe business_api presentation status (Draft|Pending|Settled|Canceled)the coarse, user-facing projection shown in the app (§9)

Concretely, the code is expected to provide, per PA adapter:

plus the AFNOR projection (internal ↔ code 200–213) and the business-app projection (internal → the presentation status, §9). A second PA simply adds its own *_from_<pa>_data / *_to_<pa>_data adapter implementing the same trait; the domain model, the AFNOR status, and the internal status set do not change. The transmission record persists the raw PA payload (API status + CDAR) so the canonical status is always reconstructable and a future migration to another PA loses nothing.

The same mappers also carry external error codes — PPF / annuaire / platform (B2Brouter) error strings — into a canonical internal error model, so that domain code branches on an internal error variant, never on a raw B2Brouter/PPF error string. Adding or remapping an external error code is a mapper change, not a domain change.

Design rule — the internal model never drops information for lack of a native B2Brouter field. If an AFNOR status (or field) has no documented native B2Brouter transmission equivalent, we still keep it in our canonical internal model. The correspondence matrix (the mappers) is what bridges internal ↔ external; the absence of a native B2Brouter field never forces us to drop information from the internal model. Recommended buyer-side statuses (204, 206, 207, 208, 209, 211) that B2Brouter’s mark_as does not expose natively are represented internally and carried/derived via the matrix and CDAR, not dropped. The exact native-transmission capability of each status is a wire-level detail of the PA adapter (confirmed against B2Brouter staging at implementation and folded into the mapper), not a property of the canonical model.

Invariant: Domain code (invoicing, bills) reads/writes the canonical internal status, the AFNOR status, and the canonical internal error model; it never branches on a B2Brouter-specific string, a raw PPF/annuaire error code, or a presentation label. All external vocabularies (B2Brouter, AFNOR codes, business-app labels, external error codes) are confined to their mappers.

2. Layer 1 — AFNOR XP Z12-012 Legal Statuses

AFNOR XP Z12-012 (February 2026 version, in force), with the DGFiP external specifications, defines the regulated invoice lifecycle as 14 statuses, codes 200–2134 mandatory (reported to the PPF concentrator) plus 10 recommended (standardized inter-platform statuses that circulate between the two parties’ platforms but are not reported up to the PPF).

Design rule — Green-Got adopts the full standard. Green-Got implements 100% of the AFNOR XP Z12-012 statuses — all 4 mandatory and all 10 recommended. The platform is conformant on the mandatory minimum and also emits/consumes every recommended status. The internal model below carries the full set so no standard status is lost.

2.1 Mandatory statuses (reported to the PPF)

CodeAFNOR statusEnglishMeaning
200DéposéeDepositedInvoice submitted to the sender’s PA and validated; ready for transmission. The entry point of the legal lifecycle.
213RejetéeRejected (by platform)Technical / format non-compliance detected by a platform; the invoice is returned to the sender and does not reach the buyer. A platform-level rejection, not a business decision. Terminal.
210RefuséeRefused (by buyer)The buyer rejects the invoice on business grounds (wrong amount, disputed goods, etc.). A buyer decision, distinct from a platform rejection. Terminal.
212EncaisséeCashed / payment receivedPayment confirmed. At the AFNOR legal layer, Encaissée (212) SUPPORTS partial collection: each partial collection is its own Encaissée flow carrying its own payment-data message (MEN — montant encaissé net) with the partial amount and collection date, so a fully-collected invoice may produce several successive Encaissée/MEN events. The “no partial settlement, full-amount-only” behaviour is a Green-Got PRODUCT choice for the customer-facing payment LINK (the link collects the full amount in one go and only then drives the AR commercial status → Paid; Settled is reserved for this AFNOR-legal Encaissée image and the coarse business_api projection), not a limitation of the AFNOR legal model. Real bank-level collection — including partial or corrected collection that occurs outside the link — is modelled in the payment-allocation / VAT-collection ledger (10. Integration Contracts §11), which carries the payment data (per VAT rate) used for VAT-return pre-fill on the collections basis. That ledger is what feeds the Encaissée/MEN payment-data reporting (see §13). Terminal for the paid path once the full amount is collected.

Invariant: These four statuses are the legal minimum reported to the PPF concentrator. Green-Got’s persistence model must always be able to produce the correct one of these for any invoice in the legal lifecycle.

2.2 Recommended statuses (inter-platform; not reported to the PPF)

CodeAFNOR statusEnglishSide that emits itMeaning
201Émise par la plateformeIssued by platformIssuer PAThe sender’s PA has transmitted the invoice to the recipient’s PA.
202Reçue par la plateformeReceived by platformRecipient PAThe recipient’s PA has received the transmission. Pre-decision.
203Mise à dispositionMade availableRecipient PAThe invoice is visible/accessible to the recipient in their tool.
204Prise en chargeTaken in chargeBuyerThe buyer acknowledges receipt and begins processing; the payment clock starts.
205ApprouvéeApproved / AcceptedBuyerThe buyer fully accepts the invoice (good-to-pay). Positive counterpart of Refusée.
206Approuvée partiellementPartially approvedBuyerPartial acceptance (partial quantity/amount, or conditional).
207En litigeIn disputeBuyer or supplierThe invoice is contested without full refusal; suspends the payment period. Non-terminal; resolves to Approuvée, Refusée, or cancellation.
208SuspendueSuspendedBuyer or PAProcessing suspended pending a missing document or clarification.
209ComplétéeCompletedBuyerSupplementary elements/documents have been provided to resolve a prior gap (e.g. lift a suspension).
211Paiement transmisPayment transmittedPayer PAPayment has been initiated/ordered; awaiting clearing/settlement. Precedes Encaissée (212).

Design rule — mandatory vs optional on the FR Flux 1 path. On the domestic B2B Flux 1 path, only the four mandatory statuses (Déposée 200, Rejetée 213, Refusée 210, Encaissée 212 — §2.1) are required to be produced/reported. The recommended statuses 201–209 and 211 are OPTIONAL: a conformant counterparty platform may emit them, and Green-Got must handle them if received (project them onto the canonical internal model per §1.1), but Green-Got must never require them to advance an invoice. In particular Prise en charge (204) is OPTIONAL — treat it as an informational acknowledgement that starts the payment clock when present; never block, gate, or wait on it, and never fail an invoice that jumps from Mise à disposition (203) / delivery straight to Approuvée (205) / Refusée (210) without a 204 ever arriving. The same “handle-if-received, never-require” rule applies to 201, 202, 203, 206, 207, 208, 209, and 211.

Note — statuses that are NOT distinct AFNOR codes. “Validée” (pre-deposit validation) and “Partiellement payée” are not lifecycle codes. Partial payment is tracked as an outstanding-balance amount (axis 2) plus one or more payment-allocation ledger entries (axis 3 — the VAT-collection record that feeds the Encaissée/MEN payment-data reporting), not as a separate invoice status. Note this is distinct from the AFNOR Encaissée (212) layer itself, which does support partial collection (each partial is its own Encaissée/MEN — see §2.1 and §13); the Green-Got internal projection keeps its current status until the customer-facing link is paid in full, reaching Settled (internal image of 212) only then — a Green-Got product choice for the link, not a limit of the legal model. The outstanding balance and the VAT-collection ledger are distinct axes from the lifecycle status; see §13 and 10. Integration Contracts §7.1. Do not confuse partial payment with Approuvée partiellement (206), which is partial approval of the invoice (a different concept). “Annulée” (cancellation) is an exceptional rare-error case outside the 14-status lifecycle and is never a substitute for a credit note (avoir, document type 381) — see the invoicing credit-note model.

2.3 Typical legal flow

Déposée (200) → Émise par la plateforme (201) → Reçue par la plateforme (202) → Mise à disposition (203)
  → Prise en charge (204) → Approuvée (205) | Approuvée partiellement (206) → Paiement transmis (211) → Encaissée (212)

with Rejetée (213) (platform rejection, terminal, correct-and-re-deposit), Refusée (210) (buyer refusal, terminal), En litige (207), Suspendue (208) / Complétée (209) as branch/intermediate states. The full legal state machine is in §4.

Design rule: AFNOR statuses are exchanged over Flux 1 (B2B domestic). The e-reporting Flux 10 lifecycle (tax-report statuses) is a separate lifecycle, not part of the AFNOR invoice statuses — see §7.

3. Layer 2 — B2Brouter API Statuses

B2Brouter (the PA) exposes the operational state of each invoice object via its REST API and webhooks. The vocabulary differs by direction, and there is a third, independent lifecycle for tax reports.

The full list of states is retrievable at GET /invoice_states. Statuses arrive at Green-Got primarily via scheduled polling / list-get reconciliation (GET /accounts/{account}/events?invoice_id={id}), which is the authoritative source of record; webhooks (HTTP POST on state change) are a non-authoritative hint that may accelerate an early poll — see §8 and the B2Brouter statuses & webhooks doc b2brouter/6. Statuses & Webhooks.

3.1 Issued (outbound) API statuses

B2Brouter statusMeaning
newInvoice created in B2Brouter, not yet sent.
sendingSend initiated; transmission to the PPF / recipient PA in progress.
sentTransmitted by B2Brouter to the PPF / recipient PA.
acceptedRecipient (buyer) accepted the invoice.
registeredTax-reported / registered (the transmission has been registered with the authority).
refusedRecipient (buyer) refused the invoice on business grounds.
paidThe generic marked-paid value (payment status; B2Brouter does no settlement — see §3.4). On the France issued lifecycle the observed CDAR 212 / Encaissée state is allegedly_paid, not plain paid; paid is the generic mark_as command value — see §5.3.
closedTerminal/closed state.
errorTransmission/processing error (includes platform rejection).
downloadedThe generated document was downloaded.

3.2 Received (inbound) API statuses

B2Brouter statusMeaning
newManually imported received invoice (issued=false).
receivedArrived via a transport (Peppol, email, B2Brouter network).
invalidValidation issue on the received document.
acceptedMarked accepted by the receiver.
refusedMarked refused by the receiver.
paidMarked paid by the receiver (payment status, metadata only). Generic / non-France provider capability only — NOT a confirmed state on the production France received path; held behind staging / provider-support (see §14).
annotatedInternal annotation state; no notification is emitted.

Receiver state changes are driven by Green-Got via POST /invoices/{id}/mark_as. On the production France received path the only valid targets are accepted / refused: { "state": "accepted|refused", "reason": "...", "commit": "with_mail" } (with_mail emails the sender). The generic paid target is a non-France provider capability held behind staging / provider-support and is not used on the France path — see §14.

3.3 Tax-report lifecycle statuses

The e-reporting tax report (Flux 10) has its own lifecycle, independent of the invoice statuses. Two scopes must be distinguished: the generic B2Brouter tax-report state set (the full set the provider can expose across all countries) and the narrower DGFiP-expected France state sets for Flux 1 and Flux 10.

Generic B2Brouter tax-report states (provider-level; not all are valid France states):

Tax-report statusMeaning
newTax report created, not yet sent.
sentSubmitted to the authority.
acknowledgedReceipt acknowledged by the authority.
registeredRegistered by the authority (success terminal).
refusedRejected by the authority.
errorSubmission/processing error.
annulledCancelled.
registered_with_errorsRegistered, but with errors flagged. Generic B2Brouter only — NOT a DGFiP status; never treat it as an expected France state unless staging proves it.

DGFiP-expected France state sets (the only states expected on the France path):

FlowDGFiP-expected states
Flux 1 (e-invoicing)new, sent, acknowledged, registered, refused, error, annulled
Flux 10 (e-reporting)new, sent, acknowledged, registered, refused, error

registered_with_errors is not in either DGFiP set; it is retained only in generic B2Brouter handling.

Design rule: Do not map tax-report statuses onto invoice AFNOR statuses. They belong to a separate obligation (e-reporting) and are projected onto a separate Green-Got internal model — see §7 and 7. E-reporting (Flux 10).

3.4 Payment status is metadata only

Invariant: B2Brouter performs no payment processing or settlement. The paid API status and the AFNOR Encaissée status are set from payment information Green-Got owns (collection on its own rails). Payment fields on the B2Brouter invoice (payment_method, iban, remittance_information, …) are metadata only.

3.5 CDAR messages

CDAR (Compte-Rendu d’Acceptation/Rejet) are the PPF/inter-platform lifecycle receipts. They are not a separate Green-Got status enum: a CDAR message carries an AFNOR lifecycle status code (200–213) — it is the transport that conveys the recommended statuses (Reçue 202, Mise à disposition 203, Prise en charge 204, Approuvée 205, …) and the mandatory ones between platforms.

Design rule — read the AFNOR code from the CDAR directly. When a CDAR is present, the AFNOR legal status is the code the CDAR carries, taken directly — it is not inferred from B2Brouter’s coarser API status. B2Brouter also reflects the outcome onto the invoice object’s API status (e.g. an acceptance receipt also drives accepted), which is the fallback when no explicit CDAR code is surfaced. Green-Got persists the raw CDAR event (with its AFNOR code) alongside the API status, so the exact legal status — mandatory or recommended — is always reconstructable. The adapter function *_from_b2brouter_data (see §1.1) reads the CDAR code first, then falls back to the API status.

4. Legal Lifecycle State Machine

The AFNOR XP Z12-012 legal lifecycle (Layer 1), independent of B2Brouter vocabulary. Mandatory statuses are marked.

stateDiagram-v2
    [*] --> Deposee: 200 Déposée (submit to PA, mandatory)

    Deposee --> Rejetee: 213 Rejetée (platform, mandatory)
    Rejetee --> Deposee: correct and re-deposit

    Deposee --> Emise: 201 Émise par la plateforme
    Emise --> Recue: 202 Reçue par la plateforme
    Recue --> MiseADispo: 203 Mise à disposition
    MiseADispo --> PriseEnCharge: 204 Prise en charge

    PriseEnCharge --> Approuvee: 205 Approuvée
    PriseEnCharge --> ApprouveePart: 206 Approuvée partiellement
    PriseEnCharge --> Refusee: 210 Refusée (buyer, mandatory)
    PriseEnCharge --> EnLitige: 207 En litige

    Approuvee --> EnLitige: 207 dispute raised
    EnLitige --> Approuvee: resolved in favour
    EnLitige --> Refusee: resolved against (mandatory)

    PriseEnCharge --> Suspendue: 208 Suspendue
    Suspendue --> Completee: 209 Complétée
    Completee --> PriseEnCharge: resume

    Approuvee --> PaiementTransmis: 211 Paiement transmis
    ApprouveePart --> PaiementTransmis: 211
    PaiementTransmis --> Encaissee: 212 Encaissée (mandatory)
    Approuvee --> Encaissee: 212 paid in full

    Rejetee --> [*]: terminal (returned to sender)
    Refusee --> [*]: terminal
    Encaissee --> [*]: terminal

    note right of Rejetee
        Rejetée (213) = rejected by a PLATFORM (technical/format).
        Distinct from Refusée (210) = refused by the BUYER (business).
        Cancellation (Annulée) is a rare error case outside the 14
        codes and is never a substitute for a credit note (avoir, 381).
    end note

Key characteristics:

5. OUTBOUND (Issued) Status Flow

Outbound is the AR path: Green-Got’s invoicing crate issues an invoice, the plateforme_agreee crate transmits it via B2Brouter, and the legal status flows back.

5.1 Outbound mapping (issued)

This is the principal outbound flow. The complete mapping, including every recommended status (codes 201–209, 211), is the canonical table in §10; the rows below show the main path.

AFNOR legal statusB2Brouter API status (issued)Green-Got internal status (invoicing)Trigger / source
(none — pre-legal)(none)DraftGreen-Got: invoice created/edited, not finalized.
(created in PA)newIssuedGreen-Got: invoice finalized + created in B2Brouter (POST /invoices).
Déposée (200)sendingSubmittedGreen-Got: POST /invoices/send_invoice/{id}; B2Brouter begins transmission.
Déposée (200)sentSubmittedB2Brouter: transmitted to PPF / recipient PA (webhook).
Rejetée (213)errorRejectedB2Brouter / PPF: platform format/technical rejection (webhook + CDAR). Never reaches buyer.
Reçue par la plateforme (202)sent (+ CDAR)DeliveredPPF/recipient PA: delivery receipt (CDAR) — delivered to buyer’s platform.
Approuvée (205)acceptedAcceptedBuyer (via recipient PA → CDAR → webhook): business acceptance.
Refusée (210)refusedRefusedBuyer (via recipient PA → CDAR → webhook): business refusal.
En litige (207)accepted / error (+ CDAR)DisputedBuyer/PPF: dispute raised (CDAR).
Encaissée (212)observed allegedly_paid (CDAR 212); command mark_as {state:"paid"}SettledGreen-Got: explicit MarkOutboundInvoicePaid command on full payment confirmed on Green-Got rails (distinct from PaymentCollected/Flux 10 — see §13). The observed France issued-lifecycle state for CDAR 212 / Encaissée is allegedly_paid, not plain paid; paid is the generic mark_as command value Green-Got sends, not the observed state it reads back (see §5.3).
(registration of e-reporting)registered(reflected on tax-report projection, see §7)B2Brouter: tax registration of the transmission.

The recommended intermediate statuses — Émise par la plateforme (201), Mise à disposition (203), Prise en charge (204), Approuvée partiellement (206), Suspendue (208), Complétée (209), Paiement transmis (211) — arrive as CDAR codes and project to the internal statuses Transmitted, Available, TakenInCharge, PartiallyApproved, Suspended, Completed, PaymentTransmitted respectively. See §10.

Design rule: registered (issued) reflects e-reporting registration of the transmission, not an AFNOR invoice status. It updates the tax-report projection, not the invoice’s legal status.

Design rule: Encaissée / paid / Settled are driven by Green-Got’s own payment/collection data, never by B2Brouter (which does no settlement). See §3.4.

5.2 Outbound sequence

sequenceDiagram
    autonumber
    participant INV as invoicing (AR)
    participant PA as plateforme_agreee
    participant B2B as B2Brouter (PA)
    participant PPF as PPF / recipient PA
    participant GG as Green-Got payments

    INV->>PA: finalize invoice → transmit
    PA->>B2B: POST /invoices (create)
    Note over B2B: API new · AFNOR Validée · internal Issued
    PA->>B2B: POST /invoices/send_invoice/{id}
    Note over B2B: API sending/sent · AFNOR Déposée · internal Submitted
    B2B->>PPF: route via Flux 1
    PPF-->>B2B: CDAR delivery receipt
    B2B-->>PA: webhook (sent) → AFNOR Reçue · internal Delivered
    PPF-->>B2B: CDAR accept/refuse (buyer decision)
    alt Buyer accepts
        B2B-->>PA: webhook (accepted) → AFNOR Acceptée · internal Accepted
    else Buyer refuses
        B2B-->>PA: webhook (refused) → AFNOR Refusée · internal Refused
    else Platform rejects (format/technical)
        B2B-->>PA: webhook (error) → AFNOR Rejetée · internal Rejected
    end
    GG->>PA: payment confirmed (Green-Got rails)
    Note over PA: AFNOR Encaissée · internal Settled
    PA-->>INV: eventbus status event (reflect legal status on AR invoice)

5.3 Command paid vs observed allegedly_paid

Terminology box — three distinct paid/Encaissée tokens, never collapse them.

TokenLayerWhat it isWhere it appears
paidB2Brouter API — commandThe generic mark_as command value Green-Got sends (POST /invoices/{id}/mark_as {"state":"paid"}). It is an input to B2Brouter, never an observed France state.the *_to_b2brouter_data mapper (outbound write)
allegedly_paidB2Brouter API — observedThe state B2Brouter surfaces on the France issued object for CDAR 212 / Encaissée (per the B2Brouter France DGFiP guide). This is what the *_from_b2brouter_data mapper reads back.the “B2Brouter API status” column of the issued tables (§3.1, §5.1, §10)
Encaissée (212)AFNOR legalThe legal lifecycle status itself.the canonical internal model + AFNOR mapper

Rule: the France ISSUED 212 / Encaissée is OBSERVED as allegedly_paid, never plain paid. Plain paid is only the generic mark_as command value. A mapper that expects plain paid as the observed France issued state, or that fails to reconstruct Encaissée from allegedly_paid, is incorrect.

Design rule — the generic mark_as command value is not the observed France state. Two different paid-related values exist and must never be conflated:

Mapper code-comment note (carry into the adapter source). Both mapper directions must carry a comment pinning this asymmetry so a future maintainer does not “simplify” the two tokens into one:

// France ISSUED 212 / Encaissée is OBSERVED as `allegedly_paid`, NEVER plain `paid`.
// `paid` is ONLY the generic `mark_as` COMMAND value we SEND (`*_to_b2brouter_data`).
// When READING back (`*_from_b2brouter_data`), reconstruct Encaissée from
// `allegedly_paid` (+ CDAR 212); do NOT expect plain `paid` on the France issued path.
// See 5_lifecycle_statuses.md §5.3 (DECISION D7).

Invariant: The status tables above use the observed France value (allegedly_paid for CDAR 212) in the “B2Brouter API status” column. A mapper that treats the generic command value paid as the observed France state — or that fails to reconstruct Encaissée when B2Brouter returns allegedly_paid — is incorrect. The exact wire strings are pinned in the mapper against staging (Uncertainties W-1).

6. INBOUND (Received) Status Flow

Inbound is the AP path: a supplier invoice arrives at B2Brouter, the plateforme_agreee crate transports it and emits an eventbus event consumed by the bills crate.

Design rule: Received supplier invoices are AP and live in the bills crate. They are never FK’d to invoicing.invoice. The plateforme_agreee crate transports the inbound document and emits an event; bills owns the received-invoice record and its internal status. See bills/2. Receiving Invoices.

6.1 Inbound mapping (received)

AFNOR legal statusB2Brouter API status (received)Green-Got internal status (bills)Trigger / source
(arrival — pre-decision)receivedReceivedB2Brouter: arrived via transport (Peppol/email/network), webhook.
(manual ingest)newReceivedGreen-Got: manual upload (issued=false).
RejetéeinvalidInvalidB2Brouter: validation issue on the received document.
AcceptéeacceptedAcceptedGreen-Got (receiver): POST /invoices/{id}/mark_as state accepted.
RefuséerefusedRefusedGreen-Got (receiver): mark_as state refused.
Encaissée ⚠️ GATEDpaid ⚠️Paid (bills, internal)Green-Got (payer): payment made on Green-Got rails. The push of mark_as paid / CDAR 212 to B2Brouter on the received French flow is NOT confirmed by B2Brouter support — see §14. The bill payment state is tracked internally in bills; it is not asserted to B2Brouter for France pending confirmation.
(internal note)annotated(no status change — annotation only)Green-Got: internal annotation; no notification.

Design rule: On the inbound side Green-Got is the buyer, so it is Green-Got that emits the confirmed buyer decisions — Acceptée (mark_as accepted) and Refusée (mark_as refused) — and these flow back to the supplier. This is the mirror image of outbound, where Green-Got receives the buyer’s decision. Encaissée / mark_as paid on the received French flow is gated (B2Brouter France support unconfirmed) — see §14; do not assert it as a confirmed received-invoice transition.

6.2 Inbound sequence

sequenceDiagram
    autonumber
    participant SUP as Supplier PA
    participant B2B as B2Brouter (PA)
    participant PA as plateforme_agreee
    participant BILLS as bills (AP)
    participant GG as Green-Got payments

    SUP->>B2B: deliver invoice (Flux 1, via PPF/Peppol)
    B2B-->>PA: webhook (received) → internal Received
    PA->>BILLS: eventbus event (inbound document received)
    Note over BILLS: bills owns the received-invoice record
    alt Validation issue
        B2B-->>PA: webhook (invalid) → AFNOR Rejetée · internal Invalid
    end
    BILLS->>PA: decision (accept/refuse)
    PA->>B2B: POST /invoices/{id}/mark_as (accepted|refused)
    Note over B2B: AFNOR Acceptée/Refusée flows back to supplier (confirmed targets)
    GG->>BILLS: payment made (Green-Got rails) → bill Paid (internal)
    Note over PA,B2B: mark_as paid / CDAR 212 for received French is GATED (§14) — not asserted to B2Brouter

Design rule — inbound arrives via polling. On the received flow, polling is the authoritative channel (plateforme_agreee polls B2Brouter on a Temporal schedule and reconciles received documents); webhooks are a notification hint that can trigger an earlier poll, not the system of record, unless B2Brouter staging proves them reliable for the French inbound flow. See 10. Integration Contracts §4.1.

7. E-reporting (Flux 10) Tax-report Lifecycle

E-reporting (B2C, cross-border B2B, payment data) travels over Flux 10 and has the separate tax-report lifecycle from §3.3. The DGFiP-expected Flux 10 states are new → sent → acknowledged → registered, or refused / error. registered_with_errors is not a DGFiP status and is kept only in generic B2Brouter handling.

Design rule: This lifecycle is not an AFNOR invoice status and not part of the invoice’s 3-layer mapping. It is projected onto a separate Green-Got internal tax-report model. Full detail lives in 7. E-reporting (Flux 10).

8. How Statuses Arrive — Webhooks, Polling, and CDAR

Mechanics (signature scheme, payload handling, idempotency, the mark_as/ack calls) are specified in b2brouter/6. Statuses & Webhooks. The inbound reception channels and mark_as flow are in b2brouter/5. Receiving Invoices.

Design rule: The B2Brouter webhook/event stream is the ingestion mechanism; the persisted raw API status + CDAR events are the source from which both the AFNOR legal status and the Green-Got internal projection are computed. Never compute the internal projection without first persisting the raw event.

9. Reconciling the Internal Status Set

The live business_api invoicing model currently uses Draft | Pending | Settled | Canceled for an issued invoice. This diverges from the lifecycle above and is too coarse to reflect the legal lifecycle (it cannot distinguish platform rejection from buyer refusal, nor delivery from acceptance).

Canonical internal status set (target design). Because Green-Got adopts 100% of the AFNOR statuses (§2), the internal set is a 1:1 image of the 14 AFNOR codes plus the pre-legal states, with English names. This keeps the mapping a bijection and the model transport-agnostic (§1.1).

Design rule: The existing business_api set (Draft|Pending|Settled|Canceled) is a presentation-layer projection of the canonical internal set, not the domain model. The API may collapse the many internal statuses for display (e.g. everything between submission and acceptance → Pending), but the domain must persist the full canonical set so every AFNOR status — mandatory and recommended — remains derivable.

business_api statusCanonical internal status(es) it projects
DraftDraft
PendingIssued, Submitted, Transmitted, Delivered, Available, TakenInCharge, Accepted, PartiallyApproved, Disputed, Suspended, Completed, PaymentTransmitted
SettledSettled
CanceledRejected, Refused, Cancelled

Invariant: The presentation projection is lossy on purpose; it MUST NOT be the persisted state. The canonical internal status, the raw B2Brouter API status, and the CDAR events are all persisted on the transmission record.

9.1 Source of truth by fact

Status source-of-truth is defined by fact, not by a single “owner of status” — that is how the two seemingly opposite statements (“GG status is a projection” vs “the internal model is the single source of truth”) are both correct (they are about different facts):

FactSource of truthNotes
Legal transmission status (what was sent/observed at PPF)B2Brouter / PPFreconstructed by plateforme_agreee from raw API status + CDAR; only projected into invoicing.
AR commercial lifecycle (issue → settle)invoicingthe canonical internal commercial state Green-Got owns.
AP processing / payment lifecycle (receive → approve → pay)billsthe canonical internal AP state Green-Got owns.
Provider integration state (PA adapter view)plateforme_agreeethe B2Brouter local/operational state and the authoritative AFNOR mapping.
Presentation status (Draft|Pending|Settled|Canceled)business_apia projection only; never authoritative.
VAT collection / payment datapayment-allocation ledger (10 §11)distinct from all status axes; the data source for the payment-data report, carried channel-specifically (Enriched212Men for invoiced Flux-1 ops, Flux10 for non-invoiced).

The full cross-doc classification (persisted-only · observed-from-B2Brouter · emitted-to-PPF/DGFiP · unsupported/gated · UI-only) is the status support matrix in 10. Integration Contracts §13.

10. The 3-Layer Mapping Table (Canonical)

This is the deliverable other docs cite. It combines outbound and inbound. “Direction” is OUT (issued, invoicing) or IN (received, bills). The mandatory AFNOR statuses are bold.

DirCodeAFNOR legal statusB2Brouter API statusGreen-Got internal statusTrigger / source
OUT(pre-legal)(none)DraftGreen-Got: finalizing the invoice.
OUT(created in PA)newIssuedGreen-Got: POST /invoices.
OUT200DéposéesendingsentSubmittedGreen-Got: POST /invoices/send_invoice/{id}; B2Brouter transmits.
OUT201Émise par la plateformesent (+ CDAR)TransmittedIssuer PA: handed to recipient PA (CDAR).
OUT202Reçue par la plateformesent (+ CDAR)DeliveredRecipient PA: received the transmission (CDAR).
OUT203Mise à dispositionsent (+ CDAR)AvailableRecipient PA: made available to the buyer (CDAR).
OUT204Prise en chargeaccepted (+ CDAR)TakenInChargeBuyer: acknowledged, processing started (CDAR).
OUT205ApprouvéeacceptedAcceptedBuyer: business acceptance (CDAR → webhook).
OUT206Approuvée partiellementaccepted (+ CDAR)PartiallyApprovedBuyer: partial acceptance (CDAR).
OUT207En litigeaccepted/error (+ CDAR)DisputedBuyer: dispute raised (CDAR).
OUT208Suspendue(+ CDAR)SuspendedBuyer/PA: processing suspended (CDAR).
OUT209Complétée(+ CDAR)CompletedBuyer: supplementary elements provided (CDAR).
OUT210RefuséerefusedRefusedBuyer: business refusal (CDAR → webhook).
OUT211Paiement transmisobserved via CDAR 211 (not plain paid)PaymentTransmittedPayer PA: payment initiated/ordered (CDAR).
OUT212Encaisséeobserved allegedly_paid (CDAR 212); command mark_as {state:"paid"}SettledGreen-Got: full payment confirmed (Green-Got rails). Observed France state is allegedly_paid, not plain paid — see §5.3.
OUT213RejetéeerrorRejectedPPF/platform: format/technical rejection (CDAR + webhook).
OUT(Annulée — rare)closed/errorCancelledGreen-Got/PPF: cancellation (rare error).
OUT(e-reporting reg.)registered(tax-report projection)B2Brouter: tax registration (Flux 10).
IN(arrival)received (or new if manual)ReceivedB2Brouter: arrival via transport / manual upload.
IN213RejetéeinvalidInvalidB2Brouter: validation issue on received doc.
IN203Mise à dispositionreceivedAvailableRecipient PA (us): made available in-app.
IN204Prise en chargemark_asTakenInChargeGreen-Got (buyer): acknowledged, processing started.
IN205ApprouvéeacceptedAcceptedGreen-Got (buyer): mark_as accepted.
IN206Approuvée partiellementmark_asPartiallyApprovedGreen-Got (buyer): partial acceptance.
IN207En litigemark_asDisputedGreen-Got (buyer): dispute raised.
IN208Suspenduemark_asSuspendedGreen-Got (buyer): suspended pending document.
IN209Complétéemark_asCompletedGreen-Got (buyer): document provided.
IN210RefuséerefusedRefusedGreen-Got (buyer): mark_as refused.
IN211Paiement transmismark_asPaymentTransmittedGreen-Got (payer): payment initiated.
IN212Encaissée ⚠️ gatedpaid ⚠️ gatedPaid (internal)Green-Got (payer): payment made; bill Paid tracked in bills. mark_as paid / CDAR 212 push to B2Brouter is gated for received French invoices (§14).
IN(annotation)annotated(no change)Green-Got: internal annotation, no notification.

Design rule — internal adoption is 100%; B2Brouter emission may be narrower. Green-Got’s internal model carries every AFNOR status (mandatory + recommended) regardless of B2Brouter. B2Brouter’s documented POST /invoices/{id}/mark_as only exposes accepted | refused | paid. The rows marked are recommended buyer-side statuses with no documented native B2Brouter mark_as — they are recorded internally only (and, where useful, as a non-legal annotation). Fail closed: never project an unsupported legal status onto a “closest” supported one. An unsupported AFNOR status (e.g. Disputed 207, Suspended 208, PartiallyApproved 206, PaymentTransmitted 211) is never emitted to the supplier/PPF as accepted, refused, or paid — doing so would assert a false legal lifecycle fact. Such statuses stay internal/annotation-only unless B2Brouter staging or support confirms an exact native wire value and its legal meaning. The exact native-transmission capability of each (the wire value B2Brouter accepts, if any) is confirmed against B2Brouter staging at implementation and folded into the mapper; it is a mapper-layer detail and never changes the canonical internal model, which always carries the full set (see §1.1). The adapter (*_to_b2brouter_data, §1.1) owns this narrowing, and a future PA adapter can transmit the full set if that PA supports it.

Note — this is the status half of the format-transposition mapping; together with §9 it covers all four representations. The table above maps three of the four status representations — AFNOR legal status ↔ B2Brouter API status ↔ Green-Got internal status, in both directions. The fourth — the coarse business-app presentation status (Draft|Pending|Settled|Canceled) — is covered by the projection table in §9. Taken together, §10 + §9 are the complete status correspondence across all four representations (AFNOR legal ↔ B2Brouter API ↔ Green-Got internal ↔ business-app), and they are the status half of the canonical-model ↔ external-format transposition. The field half — how every invoice field maps across EN 16931 / Factur-X / UBL / CII / B2Brouter JSON / the business app — lives in 4. Formats and Invoice Data §9.

Design rule (summary of the source-of-truth split):

13. Payment Collection vs Settlement vs Legal Status

Three things are routinely confused; they are separate axes (see the five axes in 10. Integration Contracts §7.1):

ConceptWhat it isEventEffect on AFNOR legal status
Settlement (AR)the payment link is paid in fullTransactionMatcheddrives AR commercial status → Paid (commercial-axis terminal); does not by itself set a legal status (the AFNOR-legal Settled/Encaissée is a separate axis)
VAT collectiona real bank movement allocated to a document with a VAT breakdown (the payment-allocation ledger10 §11)PaymentCollectedthe data source for payment-data reporting; for an invoiced Flux-1 op it feeds the enriched 212/Encaissée MEN, for a non-invoiced op (B2C/intl) the separate Flux 10 payment-data flow. The bare ledger write does not by itself change the AFNOR lifecycle status
AFNOR Encaissée (212)the legal lifecycle status — and, for invoiced ops, the payment-data carrierexplicit MarkOutboundInvoicePaid command (triggered by the same settlement, when legally applicable)sets the AFNOR legal status; for an invoiced Flux-1 op the enriched 212/MEN (collection date + amount by VAT rate, BR-FR-CDV-14) IS the payment-data report under CGI art. 290 A — not a separate Flux 10 submission. (The mark_as paid transition is documented — command paid, observed allegedly_paid/212; the MEN enrichment + correction remains staging/support-gated before VAT-on-collection launch — see 7. E-Reporting §6 gate.)

Design rule — collection feeds reporting, not the lifecycle. The event that feeds payment-data reporting is PaymentCollected (the allocation/collection event), not TransactionMatched. PaymentCollected writes the payment-allocation ledger and feeds the Encaissée/MEN payment-data reporting; it does not change the invoice’s internal Settled projection by itself. The customer-facing payment link remains full-amount-only — this is a Green-Got product choice for the link, not the legal model: the link collects the full amount in one go and only then drives the AR commercial status → Paid (commercial-axis terminal; the AFNOR-legal Settled/Encaissée is a separate axis). At the AFNOR legal layer, partial collection IS supported: each partial collection is its own Encaissée (212) / MEN with the partial amount, so the ledger may emit several successive partial Encaissée/MEN events for one invoice even though Green-Got’s link never settles partially. The ledger models the real bank-level collection — including partial or corrected collection outside the link — needed for VAT-on-collection reporting. See 10. Integration Contracts §7.

Design rule — one bank settlement, three distinct projections. A single incoming bank settlement can fan out into three separate facts that must never be collapsed:

  1. TransactionMatched — the reconciliation fact; drives the AR commercial status → Paid only (the commercial-axis terminal — the AFNOR-legal Settled/Encaissée is a separate axis). It does not, by itself, set any legal status or emit any tax report.
  2. PaymentCollected — the bank-level VAT-collection ledger entry (seller-side AR-only, keyed on invoice_id); it feeds Flux 10 payment data when VAT is on collection. This is the payment-data source.
  3. MarkOutboundInvoicePaid — an explicit command, triggered by the same settlement (TransactionMatched) when legally applicable (CGI art. 290 A / VAT-on-collection), that produces the AFNOR legal status 212 (Encaissée) and pushes it to the recipient’s PA.

MarkOutboundInvoicePaid is distinct from PaymentCollected: the same bank collection can cause both, but they are different projectionsMarkOutboundInvoicePaid drives the document’s AFNOR lifecycle status (and, for an invoiced Flux-1 op, the enriched 212/MEN that carries the payment data), while PaymentCollected is the bank-level ledger entry that is the data source for that MEN (or, for a non-invoiced op, for the separate Flux 10 payment-data flow). Neither is TransactionMatched (which is never a tax trigger). Note the nuance: the bare 212 status transition is not itself a “report”; it is the 212 enriched with the MEN that carries the payment data for invoiced ops — so Encaissée is not “never relevant to reporting” (it is the carrier for invoiced ops), but it is reached via MarkOutboundInvoicePaid, never via TransactionMatched. See 7. E-Reporting.

Invariant: TransactionMatched must not be the event that feeds payment-data reporting for service invoices. A dedicated collection/allocation event (PaymentCollected) does, and it writes the ledger and a ComplianceEventLog record (10 §12).

14. Inbound (Received) Status is Gated for France

For a received (inbound, AP) French invoice, B2Brouter’s France / DGFiP guide confirms only two valid POST /invoices/{id}/mark_as target states: accepted and refused. A generic mark_as paid — and the corresponding CDAR 212 / Encaissée on the received side — is NOT confirmed for the French received flow.

Design rule — do not assert mark_as paid for received French invoices. Until B2Brouter support confirms it on staging, the platform must not push paid / CDAR 212 to B2Brouter for a received French invoice. Green-Got’s bill payment state is tracked internally in bills and is not asserted to the provider for the French inbound path. The §6.1 and §10 rows for inbound 212 are marked ⚠️ gated accordingly.

Design rule — inbound has four separate axes. A received French invoice carries four distinct states, never one enum: Green-Got bill payment state (bills) · supplier-invoice acceptance/refusal state (bills business decision) · B2Brouter local state (plateforme_agreee) · PPF/AFNOR status (reconstructed). See the inbound axes table in 10. Integration Contracts §4.

Design rule — inbound is polling-authoritative. The authoritative inbound channel is polling on a Temporal schedule, not webhooks, unless B2Brouter staging proves webhooks reliable for the French inbound flow. Webhooks are a hint that may trigger an earlier poll. See 10. Integration Contracts §4.1.

11. Related Documents

12. Sources

Source-refresh gate (read before implementing). Re-verify the AFNOR and DGFiP status pins against the live source immediately before implementation. In force as of this revision: AFNOR XP Z12-012 February 2026 (replaces the November 2025 version — do not cite “updated 2025-07-31”); DGFiP external specifications v3.1 (published November 2025; in force for the pilot from June 2026 and the 2026-09-01 mandate) for the Flux 1 / Flux 10 status sets. Treat third-party status summaries as non-authoritative.

Source-refresh gate — CIBS recodification (legal article ids). Any CGI article id cited in this document (e.g. CGI art. 290 A for VAT-on-collection / payment-data, referenced in §13) must be re-verified before live deploy: the Code des impositions sur les biens et services (CIBS) recodification is renumbering parts of the CGI, so the cited article ids may move. Confirm each id against the live Légifrance / BOFiP text at the source-refresh gate before going live.

6. Annuaire and Routing

Annuaire and Routing

This document describes how a French B2B e-invoice is routed to the correct recipient platform: the role of the national Annuaire, the routing identifiers and levels, the routing algorithm, and the critical distinction between the national Annuaire and B2Brouter’s own directory lookup.

1. Terminology

2. The central Annuaire (PPF directory)

The Annuaire is the national routing directory operated by the PPF. Its purpose is single: given a buyer’s identifier, return the PA that buyer has designated to receive invoices, so the sender’s PA knows where to deliver.

Key characteristics:

Invariant: the Annuaire is the single source of truth for “which PA serves SIREN X”. A sender’s PA resolves the recipient platform exclusively from the Annuaire (or from its own internal knowledge when the recipient is on the same PA).

3. Routing identifiers and levels

A recipient is addressed at one of three granularities. The level is chosen by the recipient when it declares its routing scope in the Annuaire.

LevelIdentifier formSchemeGranularityWhen used
Legal entity0225:SIREN0225Whole company (all establishments)Default; one platform receives for the entire legal entity.
Establishment0225:SIREN_SIRET0225A specific establishment (SIRET)When distinct establishments route to different platforms or inboxes.
Internalinternal routing code(declared)A department / service within the entityWhen the company subdivides delivery below the establishment level.

Design rule: Green-Got resolves the recipient by buyer SIREN as the primary key (mandatory per 4. Formats and Invoice Data §4), refining to SIRET or an internal code only when the buyer designates one. The buyer’s chosen routing level is owned by the buyer’s Annuaire declaration, not by the sender. On the recipient side this is no constraint — B2Brouter performs the Annuaire lookup at any granularity the buyer declared. The granularity table above describes the Annuaire address space; the sender-side support limitation (what scope Green-Got can declare for its own customers) is separate and covered in the next design rule: only SIREN-level is supported today.

Design rule — Green-Got customers default to SIREN-level scope; finer scope is UNSUPPORTED until confirmed. For Green-Got’s own customers (the sender side), Green-Got declares the customer’s Annuaire routing scope at enrollment. The default — and currently the only supported — scope is SIREN-level (0225:SIREN, the whole legal entity), which matches B2Brouter’s one-account-per-SIREN model and registers automatically when the dgfip tax-report setting is enabled, with no extra provider call.

SIRET-level (0225:SIREN_SIRET) and internal-code scope are UNSUPPORTED until the exact B2Brouter mechanism is confirmed. B2Brouter publishes no documented endpoint or field for choosing routing-scope granularity: account creation accepts only cin_scheme/cin_value (0002 SIREN / 0009 SIRET) and dgfip tax-report settings carry no scope selector; B2Brouter derives the SIREN from any SIRET and consolidates all routing under one account, and separate-establishment routing is handled as “organisational units” arranged through B2Brouter Support, not via an API field. The read-only GET /directory/... lookup (§5) exposes cinX_scheme/cinX_value routing-code fields and France’s 8017 “code service” scheme in responses, but there is no documented write path to set them. Until the exact endpoint/field is documented and confirmed on staging, Green-Got offers SIREN-level only and does not ship a UI option whose backing provider call is unverified. [open — provider-support] (does a routing-scope-granularity endpoint/field exist, and what is it?) and [open — staging-wire] (confirm the exact call and its effect on the Annuaire entry on staging before enabling non-SIREN scope). See 9. Mandate and Onboarding §4.4, which makes the same SIREN-only decision.

Invariant — enforce SIREN-only at enrollment (until uncertainty P-2 closes). The SIREN-only limitation above must be enforced, not merely documented. At enrollment, when Green-Got declares a customer’s Annuaire routing scope, it MUST register SIREN-level scope (0225:SIREN, the whole legal entity) and MUST NOT expose, accept, or transmit any SIRET-level (0225:SIREN_SIRET) or internal-code routing scope to B2Brouter. Concretely: the enrollment surface exposes no routing-scope-granularity input; the B2Brouter account is created with the SIREN-derived identity only; and any attempt (UI, API, or import) to declare a finer scope is rejected at enrollment rather than silently downgraded. This invariant holds until uncertainty P-2 (the routing-scope-granularity provider mechanism) is resolved and the exact B2Brouter write path is confirmed on staging; only then may finer scope be enabled behind that verified call.

4. The routing algorithm

4.1 Numbered flow

  1. The issuer (Green-Got customer) builds the invoice with a mandatory buyer SIREN (optionally SIRET / internal code).
  2. Green-Got submits the structured invoice to its PA, B2Brouter.
  3. B2Brouter validates the invoice (format + business rules).
  4. B2Brouter resolves the recipient: it queries the Annuaire with the buyer’s routing identifier (0225:SIREN, or the finer key).
  5. The Annuaire returns the recipient PA’s routing address.
  6. B2Brouter routes the invoice to the recipient PA (or, if the recipient is on B2Brouter too, delivers internally — the same-PA P2P shortcut).
  7. The recipient PA delivers the invoice to the buyer.
  8. Lifecycle statuses flow back along the same path (recipient PA → sender PA → Green-Got) and to the PPF concentrator for DGFiP. See 5. Lifecycle Statuses.

4.2 Sequence diagram

sequenceDiagram
    autonumber
    participant GG as Green-Got (issuer)
    participant SPA as Sender PA (B2Brouter)
    participant ANN as National Annuaire (PPF)
    participant RPA as Recipient PA
    participant Buyer as Buyer
    participant PPF as PPF concentrator → DGFiP

    GG->>SPA: Submit invoice (buyer SIREN mandatory)
    SPA->>SPA: Validate format + business rules
    SPA->>ANN: Lookup buyer routing id (0225:SIREN)
    ANN-->>SPA: Recipient PA routing address
    alt Recipient served by same PA
        SPA->>Buyer: Deliver directly (P2P shortcut)
    else Recipient on a different PA
        SPA->>RPA: Route invoice to recipient PA
        RPA->>Buyer: Deliver invoice
    end
    RPA-->>SPA: Lifecycle statuses (Reçue, Acceptée/Refusée…)
    SPA-->>GG: Lifecycle statuses
    SPA->>PPF: Lifecycle status + e-reporting data

4.3 Same-PA P2P shortcut

When the Annuaire resolves the recipient to the same PA as the sender (both Green-Got’s customer and the buyer are served by B2Brouter), that PA delivers internally without a cross-platform hop. Both parties must still be Annuaire-registered; the shortcut is a delivery optimisation, not a bypass of the directory or of status reporting to the PPF.

4.4 Recipient absent from the Annuaire (domestic B2B)

Step 4–5 of §4.1 can fail: the buyer’s SIREN is not found in the national Annuaire (the recipient never declared a PA, is not yet registered, or the identifier is wrong). For a domestic B2B invoice this is an error, not a degraded delivery mode.

Design rule — recipient-not-found is a FAIL-CLOSED legal-non-transmission, surfaced as a REJECTION (Rejetée 213), never a Flux 10 fallback and never a silent success. When the Annuaire has no route for a domestic buyer SIREN, the transmission fails closed: the invoice is not legally transmitted and is surfaced as a platform-level rejection → AFNOR Rejetée (213) (internal Rejected; see 5. Lifecycle Statuses §2.1 and §10). The issuer is told the recipient is unreachable in the Annuaire and must correct the identifier and re-deposit (the standard Rejetée → correct-and-re-deposit loop).

This is not merely a routing rejection or a retry condition — it is a legal-non-transmission state that must fail closed. B2Brouter signals the same condition on the invoice object itself via in_dgfip_annuaire: false, where the invoice is created (HTTP 2xx) but generates NO Flux 1 tax report (confirmed against the B2Brouter France DGFiP guide — §8 Sources). A 2xx/created object is therefore NOT evidence of legal transmission: Green-Got applies the fail-closed post-call validation invariant (B2Brouter — Sending Invoices §5.1) and treats in_dgfip_annuaire: false (or any domestic-B2B response with no Flux 1 report attached) as not-legally-sent — surfaced, kept under deadline tracking with pre-breach escalation, and re-driven through the correct-and-re-deposit loop, never silently recorded as sent.

Green-Got MUST NOT silently down-route a domestic invoice to Flux 10 (e-reporting) because the recipient is unreachable: Flux 10 is not a delivery channel for a domestic invoiced operation, and using it to “report instead of deliver” a domestic B2B invoice is non-compliant — the operation owes an e-invoice over Flux 1, not transaction data over Flux 10. The Flux-10-for-an-unreachable-counterparty path exists only for FOREIGN recipients (a buyer with no French establishment, hence legitimately not in the Annuaire), where e-reporting is the correct obligation; it is never the fallback for a domestic buyer who simply has not registered.

Invariant — preserve the invoice number across resubmission. A recipient-not-found Rejetée does not consume or burn the invoice number. On correction and re-deposit the same invoice number (BT-1) is preserved — one invoice number, potentially several transmission attempts — so the chronological/gap-free numbering sequence is not broken by an unreachable recipient. See the re-deposit-after-Rejetée numbering/idempotency contract in 10. Integration Contracts §3.1 (the matching outbound rejection path) and invoicing/5. Numbering §7.

This domestic rejection path is fixed by the DGFiP external specifications v3.1 (published November 2025; in force for the pilot from June 2026 and the 2026-09-01 mandate), which define Annuaire routing and the Flux 1 vs Flux 10 scope boundary. The exact provider error code B2Brouter surfaces for an unresolved Annuaire route, and whether it arrives during the ≤24 h Annuaire-propagation window vs as a hard not-found, are confirmed against B2Brouter staging at implementation and folded into the canonical error mapper (5. Lifecycle Statuses §1.1).

5. National Annuaire vs B2Brouter directory lookup

There are two different directories; they must not be conflated.

AspectNational Annuaire (PPF)B2Brouter directory lookup
Operated byPPF (the State)B2Brouter (the PA)
Question answered“Which PA serves this SIREN?”“Does this recipient have a B2Brouter account / which transports does it support?”
ScopeAll ~4.5M French assujettis (Annuaire-registered entities)B2Brouter’s own network / reachable transports
Access from Green-GotIndirect — B2Brouter queries it during routing; Green-Got never calls it directlyGET /directory/{scheme}/{id} or GET /directory/{country}/{id}
RoleAuthoritative national routing sourceConvenience lookup to pre-resolve a recipient’s transport/document types before creating a contact/invoice

Design rule: Green-Got does not maintain or directly query the national Annuaire. National routing is B2Brouter’s responsibility once the parties are registered. B2Brouter’s GET /directory/... resolves B2Brouter accounts and transports only — it is a transport-resolution convenience, not the national directory. Green-Got must not treat B2Brouter’s directory response as the national routing authority, and must not persist a local “partner directory” as if it mirrored the Annuaire.

Invariant: Green-Got holds no national routing table. The buyer SIREN on the invoice is sufficient; B2Brouter performs the Annuaire lookup and routing transparently.

6. How Green-Got’s customers get registered in the Annuaire

Green-Got’s customers are registered in the national Annuaire through B2Brouter, as part of onboarding — Green-Got never writes to the Annuaire directly.

When Green-Got enables DGFiP/PPF registration for a customer account via B2Brouter (the canonical onboarding flow — create the DGFiP tax-report setting with POST /accounts/{account}/tax_report_settings, update it with PUT /accounts/{account}/tax_report_settings/dgfip; full tax_report_setting body, never a bare {code, enabled} toggle — documented in B2Brouter — Onboarding accounts §4), B2Brouter auto-registers the SIREN/SIRET in the PPF Annuaire, makes the entity discoverable as a recipient, and enables Flux 1 (domestic B2B) and Flux 10 (B2C / cross-border e-reporting).

This is the mechanism by which a Green-Got customer becomes both routable (others can send to it) and able to send. The full onboarding sequence — account creation under eDocSync, tax report settings, mandate capture — is covered in 9. Onboarding and B2Brouter — Onboarding accounts.

Design rule: Annuaire registration is a side effect of enabling the DGFiP tax report setting on the B2Brouter account. Green-Got models registration state as a property of the customer’s PA onboarding, not as a direct Annuaire write.

7. Related documents

8. Sources

7. E-Reporting

E-Reporting

This document describes the e-reporting obligation (Flux 10) — the periodic transmission of transaction and payment metadata to DGFiP — and how that obligation is split between B2Brouter (the approved platform) and Green-Got (the Solution Compatible). It covers two distinct strands: transaction data for operations outside domestic B2B e-invoicing (B2C, cross-border sales, and certain acquisitions — CGI art. 290), and payment data for operations where VAT is due on collection (CGI art. 290 A).

The payment-data strand has two carriers depending on whether the operation was e-invoiced:

Green-Got’s internal PaymentCollected ledger is the data source that feeds the enriched 212/MEN for invoiced operations (and the Flux 10 payment data for non-invoiced ones); it is not itself the legal carrier.

1. Terminology

Terms common to the whole documentation set are defined once in 1. Reform Overview. This section defines only the terms specific to e-reporting.

2. Why E-Reporting Exists and How It Differs from E-Invoicing

The reform pursues a complete view of economic activity for VAT control. Domestic B2B invoices give DGFiP that view directly, because every such invoice already transits an approved platform over Flux 1. But three categories of transaction never produce a domestic B2B e-invoice: sales to consumers, sales to or from foreign counterparties, and the moment of payment under the collection regime. E-reporting closes those gaps by transmitting metadata for exactly those categories.

E-reporting is therefore a distinct legal obligation, not a by-product of e-invoicing. A company can be subject to e-reporting without ever issuing a domestic B2B e-invoice (for example a pure-retail business reporting only B2C data).

But the converse — “a Flux 1 invoice is never e-reported” — is wrong. The transaction-data part of e-reporting does not duplicate a Flux 1 invoice, but the payment-data part (CGI art. 290 A) does apply on top of domestic B2B invoices whenever VAT is due on collection (encaissement), except reverse-charge cases. A domestic B2B service invoice on the collection regime flows over Flux 1 and, when collected, carries its payment data by enriching its Encaissée/212 status with the MEN (collection date + amount by VAT rate) — not via a separate Flux 10 submission. The separate Flux 10 payment-data flow is reserved for non-invoiced operations.

Invariant: Every operation is classified twice and independently:

  1. Transaction channel — domestic B2B is in scope for e-invoicing (Flux 1) and out of scope for e-reporting transaction data; B2C and cross-border are out of scope for e-invoicing and in scope for e-reporting transaction data (Flux 10).
  2. Payment-data overlay (art. 290 A) — independent of channel; required iff VAT is due on collection and the operation is not reverse-charge. It can therefore sit on top of a Flux 1 invoice. The carrier depends on the channel: for invoiced (Flux 1) operations the payment data rides the enriched Encaissée/212 MEN; for non-invoiced operations it rides the separate Flux 10 payment-data flow.

The two classifications are orthogonal. See 1. Reform Overview and 2. Platform Architecture.

2.1 E-invoicing vs e-reporting

DimensionE-invoicing (Flux 1)E-reporting (Flux 10)
What is transmittedA structured invoice (Factur-X / UBL 2.1 / CII)Transaction and payment metadata only
RecipientThe buyer’s approved platform, then DGFiP via the concentratorDGFiP via the concentrator (no counterparty delivery)
Transactions coveredDomestic B2B between French assujettisB2C, cross-border B2B (sales and reportable acquisitions), and payment data for non-invoiced operations
OverlapPayment data for invoiced ops rides the enriched Encaissée/212 MEN here (art. 290 A)Payment data for non-invoiced ops only — invoiced-op payment data does not use a separate Flux 10 submission
MechanismDocument exchange + lifecycle statuses (per invoice)Periodic batched metadata transmission
CadencePer invoice, on issuanceStatutory minimum transmissions by VAT regime (e.g. ≥3/month réel normal mensuel, ≥1 every two months franchise en base); separately batched into daily provider ledgers — see §4
Counterparty receives a documentYesNo
Lifecycle trackingYes — AFNOR XP Z12-012 statusesNo per-document lifecycle; reporting is aggregate/structured

3. The Article 290 Operation Taxonomy

E-reporting is not only about outbound B2C and cross-border sales. CGI art. 290 lists a set of transaction operations to report, several of which are purchases/acquisitions from non-French counterparties; art. 290 A adds the payment-data obligation. A given company may be subject to one, several, or all of these depending on its activity.

Each operation type attaches to one side of Green-Got’s ledger — AR (invoicing / sales) or AP (bills / purchases) — which determines which domain owns the data Green-Got must supply.

#Operation type (art. 290)DirectionOwning sideE-invoicing?E-reporting?
1B2C sales to non-taxable persons in FranceSaleARNoYes (transaction data)
2Intra-EU B2B sales (deliveries/services to EU assujettis)SaleARNoYes (transaction data)
3Extra-EU B2B sales (exports, services to non-EU assujettis)SaleARNoYes (transaction data)
4Intra-EU acquisitions of goods (France is destination)PurchaseAPNoYes (acquisition data)
5Purchases of goods/services from non-French suppliers where the operation is located in FrancePurchaseAPNoYes (acquisition data)
6Monaco operations (both directions, treated as domestic-equivalent for VAT)Sale / PurchaseAR / APPer naturePer nature
7Payment data (art. 290 A) for sales under the collection regime and for domestic B2B service invoicesOverlay (seller-side collection)AR onlyn/aYes when VAT due on collection

Design rule — AR vs AP e-reporting ownership:

3.1 B2C transaction data (AR)

Data describing sales to non-taxable persons (consumers). Reported by any company that sells to consumers, regardless of whether it also issues domestic B2B e-invoices, on the legal cadence for its VAT regime (see §4.1).

B2C transaction data carries a daily_transaction_count placeholder field per reporting period (alongside turnover by sale category and VAT rate). It is a tracked launch item, not yet wired (see the gate below).

[open — legal] daily B2C transaction count. The September-2026 simplification package is consistently reported as confirming removal of the per-day B2C transaction-count obligation (only turnover by sale category remains) — see EY tax alert. However, the current text of CGI ann. III art. 242 nonies M still lists “le nombre de transactions quotidiennes” for B2C operations not invoiced electronically, and the removal is not yet enacted in the live Legifrance text. Keep a Legifrance verification gate: maintain the daily_transaction_count placeholder field and treat the count as potentially still required until the final text confirms removal — do not build on its removal. See the launch gate in §4.1.

3.2 Cross-border B2B data — sales and acquisitions (AR + AP)

Data describing B2B operations with a counterparty outside France — intra-EU and extra-EU.

Reportable-acquisition decision algorithm (CGI art. 290, items 4–5). A supplier bill is a reportable acquisition iff all of:

  1. the supplier is non-French — determined from the supplier’s country/VAT-identification on the bill: no French SIREN/SIRET and a non-FR country (intra-EU VAT number with a non-FR prefix → item 4; any other non-FR supplier → item 5). A French SIREN/SIRET makes the bill a domestic purchase (already covered by the supplier’s own Flux 1 invoice) and not a reportable acquisition; and
  2. the operation is located in France for VAT (France is the place of supply / destination of the goods or the place where the service is taxable); and
  3. the customer is a VAT-liable assujetti for the operation.

When all three hold, the bills domain surfaces a ReportableAcquisitionRecorded event (see §3.3). Reverse-charge acquisitions are still reported as transaction/acquisition data (the buyer self-liquidates the VAT, but the operation is still reportable under art. 290) — the reverse-charge exclusion in art. 290 A removes only the payment-data obligation, which never applies on the AP side anyway. Where supplier country/VAT identity cannot be determined from the bill, the acquisition is surfaced for operator review rather than silently dropped.

3.3 Payment data (art. 290 A) — overlays both channels

Data describing the seller-side collection of payment, reported only when VAT becomes due on collection (encaissement) — that is, for supplies of services by default (unless the service provider opted for debits). Goods are always on debits and cannot elect collection (delivery/supply, CGI art. 269), so they never produce payment data; mixed invoices are resolved per line / per chargeability. Payment data is emitted by the entity receiving the payment (the seller / AR side); a buyer paying a supplier never emits payment data. It lets DGFiP determine the period in which the VAT became chargeable. Per art. 290 A it applies to sales operations under both domestic e-invoicing (art. 289 bis) and e-reporting (art. 290) — so it overlays Flux 1 invoices, not just Flux 10 transactions — except reverse-charge operations (VAT due by the buyer).

Each payment-data submission carries:

These fields feed the payment-allocation / VAT-collection model in the PA lifecycle & integration docs, which models real bank-level collections (the canonical ledger keys an invoice_id — it is seller-side AR-only and never carries a bill_id — plus transaction_id, allocated_amount, collection_date, currency, source, correction/reversal link, cumulative_collected_amount, and a vat_breakdown[] whose per-group reportability_state is keyed on {vat_rate, vat_chargeability} — there is no top-level reportability_state; see 10. Integration Contracts §11.1). The ledger, not the customer link, is the reporting source.

Carrier depends on the channel (decision D5). The same PaymentCollected fields feed two different legal carriers:

[gate — MEN enrichment + correction staging/support-gated — L-4 / P-7] The issued-invoice mark_as paid transition itself is documented — B2Brouter’s API reference lists paid as a valid issued-invoice mark_as target (mark-as-invoice reference), and the France DGFiP guide maps the observed issued state allegedly_paid to CDAR 212 / Encaissée (France DGFiP guide). So the paid transition is not “unavailable”: Green-Got sends the generic command mark_as {state:"paid"} and reads back the observed France state allegedly_paid (never plain paid — keep the command-vs-observed distinction, see 5. Lifecycle Statuses §5.3). What remains staging/support-gated before VAT-on-collection launch is not the existence of the transition but the enriched MEN payload that turns the bare 212 into a legal payment-data report: (a) the enriched MEN fields — collection date + amount by VAT rate (rule BR-FR-CDV-14) — and whether B2Brouter derives them from the paid/212 transition plus the invoice metadata or requires Green-Got to supply them explicitly; (b) the legal effect of the enriched 212 as the CGI art. 290 A payment-data report; and (c) the correction / reversal semantics for an already-reported MEN. Until these are confirmed on staging with provider support, no VAT-on-collection customer goes live. (Research 2026-06-22 — Uncertainties L-4.) Do not build on either the derive or the supply assumption for the MEN payload until then; the mark_as paid transition and its observed allegedly_paid state are, by contrast, documented.

Canonical trigger chain (single source of truth):

  1. TransactionMatched records commercial full settlement only — a reconciliation fact, never a tax trigger.
  2. PaymentCollected is the bank-level VAT collection/allocation ledger entry — the canonical record of an actual collection, and the data source for the payment-data obligation.
  3. Payment data is derived from a reportable PaymentCollected (collection regime, not reverse-charge, reportability_state in scope) and emitted on the channel-appropriate carrier: the enriched Encaissée/212 MEN for invoiced (Flux 1) operations, or the separate Flux 10 payment-data flow for non-invoiced operations.

For invoiced operations the Encaissée/212 status is therefore not a separate lifecycle-only projection: enriched with the MEN, it is the payment-data carrier. The rule that no payment data is emitted “by marking the invoice Encaissée” with no MEN still holds — a bare status change carries nothing; it is the enriched MEN that reports.

3.4 AP acquisition e-reporting (AP)

Reportable acquisitions (items 4–5 of §3) are transaction data only — never payment data — surfaced by the bills domain via the canonical ReportableAcquisitionRecorded event (producer bills, consumer plateforme_agreee), and transmitted over Flux 10. The reportability rule is the decision algorithm in §3.2 (non-FR supplier + France-located operation + VAT-liable buyer).

Acquisition Flux 10 payload (fields Green-Got must supply):

Reverse-charge handling. A reverse-charge acquisition (buyer self-liquidates) is reportable as acquisition/transaction data; the art. 290 A exclusion only suppresses payment data, and the AP side emits no payment data regardless.

Correction semantics (append-only). Acquisition records are append-only, mirroring the PaymentCollected ledger: a correction is a new record linked to the original via a correction_reversal_link, the original’s reportability_state becomes superseded, and the corrected record is re-reported over Flux 10. A ComplianceEventLog entry records the before/after. Nothing is mutated in place. The exact canonical schema lives in the PA integration contracts (ReportableAcquisitionRecorded, §6) — this section states the e-reporting-specific reportability, reverse-charge, and correction rules.

4. Cadence and Deadlines

“Cadence” conflates three different clocks that must not be confused. They are owned by different parties and have different drivers.

4.1 Legal reporting period (by VAT regime, not company size)

The legal minimum frequency at which a company must e-report follows its VAT regime, not its headcount. The current legal text (CGI ann. III art. 242 nonies O for transaction data, 242 nonies P for payment data) sets a minimum number of transmissions per regime, not a “reporting period”:

VAT regimeTransaction data (242 nonies O)Payment data (242 nonies P)
Réel normal — mensuelAt least 3 transmissions / monthAt least 1 / month
Réel normal — trimestrielAt least 1 transmission / monthAt least 1 / month
Régime simplifié de déclaration (replaces RSI from 2027-01-01)At least 1 transmission / monthAt least 1 / month
Franchise en base (293 B) / remboursement forfaitaireAt least 1 transmission every two monthsAt least 1 every two months

RSI abolished 2027-01-01. The former régime simplifié d’imposition (RSI / CGI art. 302 septies A) is abolished from 2027-01-01 by loi de finances 2025 art. 38before PME/micro e-reporting begins (2027-09-01), so an RSI cadence row would never be operative. It is not merged into réel normal: it is replaced by a new régime simplifié de déclaration whose VAT-filing periodicity is monthly by default, with an option for quarterly filing if turnover does not exceed €1,000,000 (prior year) and €1,100,000 (in-year); exceeding €1,100,000 in-year forces monthly from the month of the excess. The e-reporting minimums above attach to whichever effective periodicity (monthly/quarterly) the regime resolves to. Sources: LégiFiscal — réforme régime simplifié TVA 2027, Secob — suppression du RSI à compter de 2027 (LF 2025), CCI Paris IdF — régime simplifié de déclaration.

Within these statutory minimums, a budget-minister order fixes the specific deadlines, anchored to the company’s VAT filing calendar; confirm the exact dates against the final DGFiP specification. The earlier rollout framing of “tri-monthly for large/ETI” reflected the start phasing, not a size-based legal period — the driver is the VAT regime. The start dates still mirror the e-invoicing phases: large/ETI from 2026-09-01, PME/micro from 2027-09-01. See 1. Reform Overview.

[open — legal] daily B2C transaction count (launch gate). Removal of the per-day B2C transaction-count requirement (art. 242 nonies M) is consistently reported as confirmed in the September-2026 simplification package, but is not yet enacted in the live Legifrance text. Keep the daily_transaction_count placeholder (see §3.1) and a Legifrance verification gate; treat the count as potentially still required until the final text confirms removal — do not build on it.

4.2 Provider ledger batching (B2Brouter — daily)

Independently of the legal cadence, B2Brouter groups all Flux 10 tax reports from a calendar day into Ledgers transmitted to the PPF once per day (scheduled ~02:00 server time). This daily batching is a provider grouping / transport detail of the PA — NOT the legal obligation, and is finer-grained than the legal minimum: Green-Got can emit a transaction or payment event at any time, and B2Brouter accumulates it into that day’s ledger. The legal minimum (clock 1) is satisfied as long as enough events reach ledgers within each statutory window; the daily ledger run never, on its own, defines or discharges the legal obligation.

4.3 Green-Got reconciliation deadline and alerting window

Green-Got owns a reconciliation deadline ahead of the legal period close, so that a missing collection event or unmatched transaction is caught before the obligation is breached:

Design rule: Never collapse these three clocks. (1) The legal minimum cadence is keyed off the customer’s VAT regime (current legal text, 242 nonies O / P); (2) B2Brouter’s daily ledger batching is a provider grouping / transport detail, not the legal obligation; (3) Green-Got’s reconciliation cutoff + alerting is an internal safety margin that must fire before the legal deadline. No statement may treat the daily ledger run as the legal obligation.

4.4 Sanctions

The sanction for missing e-reporting transmissions falls on the customer (the assujetti), not on Green-Got or B2Brouter, which makes the §4.3 reconciliation/alerting safety net a customer-protection feature.

Source: LégiFiscal — facturation électronique : le risque de sanction, Fiducial — sanctions facture électronique 2026-2027.

5. When Payment Data Is NOT Required

Payment-data e-reporting is the most easily over-applied obligation. It is required only under the collection regime and is not required in the following cases:

Design rule: Green-Got emits payment data only for transactions under the collection (encaissement) regime, and only from a PaymentCollected ledger entry that is reportable. For every other case the payment event must produce neither an enriched Encaissée/212 MEN nor a Flux 10 payment-data submission.

6. Division of Responsibility — B2Brouter vs Green-Got

Once an account has DGFiP/PPF registration enabled, B2Brouter performs the regulated Flux 10 transmission automatically. Registration follows the canonical onboarding flow (create the tax-report setting with the full body, then enable it via the dgfip update) — see B2Brouter — DGFiP / PPF registration; this is not re-documented here. Enabling it makes the SIREN/SIRET discoverable and turns on both Flux 1 (domestic B2B) and Flux 10 (e-reporting). For the invoice/transaction data B2Brouter can derive at create/send time there is no separate e-reporting API call — B2Brouter derives that e-reporting data from the invoices and transactions it processes and batches it to DGFiP on the applicable cadence. This does not extend to the VAT-on-collection payment-data overlay: that data is Green-Got-owned (B2Brouter performs no payment processing — see “What Green-Got must supply” below), and its exact submission/correction mechanism is a launch-blocking item confirmed against staging (Uncertainties L-4, P-7), not something derived automatically from invoice submission. See also B2Brouter — sending invoices.

What B2Brouter handles:

What Green-Got must supply:

[gate — staging L-4 / P-7] The issued mark_as paid transition is documented (the API reference lists paid as a valid issued target; the France DGFiP guide maps the observed allegedly_paid to CDAR 212 / Encaissée). What is unconfirmed and staging/support-gated is the MEN enrichment + correction, not the transition: whether B2Brouter derives the enriched 212/MEN (collection date + amount by VAT rate, BR-FR-CDV-14) from the paid/212 transition plus the metadata Green-Got supplies, or whether Green-Got must supply the MEN payload explicitly; the legal effect of the enriched 212 as the CGI art. 290 A payment-data report; and the correction/reversal semantics for an already-reported MEN. Verify all three on staging before any VAT-on-collection customer goes live (Uncertainties L-4, P-7).

Design rule: The transaction channel (Flux 1 vs Flux 10) is set by operation nature; the payment-data overlay is set by the VAT-exigibility regime independently of channel; and the boundary between “B2Brouter reports automatically” and “Green-Got must supply data” is set by who holds the information. Green-Got owns the PaymentCollected ledger (the payment-data source), the VAT-regime flag, and the AP reportable-acquisition feed; B2Brouter owns the transmission. For invoiced operations the payment-data output is the enriched Encaissée/212 MEN, not a separate Flux 10 submission; for non-invoiced operations it is the Flux 10 payment-data flow.

7. Foreign Companies

A foreign company without a permanent establishment (PE) in France that carries out transactions subject to French e-reporting must also contract with an approved platform; it cannot submit e-reporting directly to DGFiP. For such customers, Green-Got provisions a B2Brouter account in the same way as for French companies, and Flux 10 is transmitted through B2Brouter.

The non-established-taxpayer obligation is NOT live from the standard date — it is deferred and phased:

Code path must gate on the 2027 dates. Non-established-taxpayer enrollment and transmission must not treat the obligation as live from the standard date: gate provisioning/transmission on the phased dates above (2026-09-01 for large/ETI sellers; 2027-09-01 otherwise and for VAT-liable buyers). Sources: impots.gouv.fr — E-reporting for foreign companies without PE, EY — September 2026 simplification measures.

8. E-Reporting Data Flow

sequenceDiagram
    autonumber
    participant GG as Green-Got (SC)
    participant Tx as Transaction matching
    participant B2B as B2Brouter (PA)
    participant PPF as PPF concentrator
    participant DG as DGFiP

    Note over GG: Account has DGFiP/PPF registration enabled (see onboarding flow)
    GG->>B2B: Submit AR B2C / cross-border SALES transaction metadata
    GG->>B2B: Submit AP reportable-acquisition data (purchases from non-FR suppliers)
    Tx->>GG: TransactionMatched — incoming payment matched, full settlement (collection regime)
    GG->>GG: Write PaymentCollected ledger entry (date, amount incl VAT, VAT by rate, currency) — payment-data SOURCE
    alt Domestic B2B invoiced (Flux 1)
        GG->>B2B: Enrich Encaissée/212 with MEN (collection date + amount by VAT rate) — carries payment data
    else Non-invoiced (intl B2B, B2C)
        GG->>B2B: Emit Flux 10 payment data from reportable PaymentCollected
    end
    Note over B2B: Groups Flux 10 reports into a daily Ledger (~02:00)
    B2B->>PPF: Flux 10 transmission (per VAT-regime legal period, ~by 15th)
    PPF->>DG: Aggregate e-reporting data
    DG-->>B2B: Acknowledgement (acknowledged / registered)
    B2B-->>GG: Tax-report lifecycle update

9. Related Documents

10. Sources

Source-refresh gate (read before implementing). Re-verify every legal pin below against the live Legifrance / AFNOR text immediately before implementation. Pins in force as of this revision: AFNOR XP Z12-012 February 2026 (replaces the November 2025 version); CGI art. 289 bis / 290 / 290 A Legifrance pages in force since 2026-02-21 (article ids LEGIARTI000053546660 / LEGIARTI000053546668 / LEGIARTI000053546674); note the CIBS recodification effective 2026-09-01. Treat third-party status summaries as non-authoritative.

8. Archiving and Audit Trail

Archiving and Audit Trail

This document describes the legal archiving obligations attached to electronic invoices under the French reform — retention periods, the piste d’audit fiable, the certified electronic archiving system (SAE) — and Green-Got’s storage model: for every invoice, store the exact legal artefact (the authoritative transmitted or received bytes) with its content hash and audit metadata; regeneration from DB data is a display convenience only. Privacy, data-minimisation, and the retention of operational / personal data — as distinct from the statutory retention of the legal invoice artefacts (6 yr VAT / 10 yr accounting) and Green-Got’s separate product archive promise beyond it (§2) — are covered in 13. Privacy and Data Protection.

1. Terminology

Terms common to the whole documentation set are defined once in 1. Reform Overview. This section defines only the terms specific to archiving and audit.

2. Retention Periods

Two distinct statutory retention durations apply to the same invoice; the longer governs the legal minimum. A third, separate basis — Green-Got’s product archive promise — extends availability beyond those statutory minimums, but on its own purpose and lawful basis, not as an extension of the statutory obligation.

BasisDurationStarting pointPurposeLawful basis
(a) VAT (fiscal) retention6 years (statutory)From the invoice date / end of the operationTax authority’s right to audit VATLegal obligation (BOFiP — 6-year fiscal)
(b) Accounting (commercial) retention10 years (statutory)From the end of the fiscal yearCommercial-code bookkeeping retentionLegal obligation (BOFiP — 10-year accounting)
(c) Green-Got product archive promiseCustomer lifetime with Green-Got, after (a)/(b) elapseEnd of the statutory windowCustomer-facing “always retrievable” product featureSeparate — needs its own purpose + lawful basis + access controls + deletion/export/offboarding policy + customer controls (see below); not “legal obligation”
(d) Permanent non-PII audit evidencePermanentRetrieval / event timeTamper-evidence and PAF integrity proofLegitimate interest — easier to justify, carries no full personal-data artefact

Statutory retention (a)/(b). For the statutory window the legal-obligation basis compels Green-Got to retain the stored legal artefact — the exact transmitted / PA-generated file for an issued invoice, the original received file for an inbound bill, the signed PDF for a signed quote — together with its content hash, its immutable structured DB data, and the audit trail (§3). The 6-year fiscal / 10-year accounting figures (BOFiP) are the period during which the tax authority can compel production; the longer (10-year accounting) sets the statutory minimum Green-Got must never fall below.

Product archive promise (c) — a distinct basis, not an indefinite legal obligation. Green-Got’s product offers customers the ability to retrieve their earliest invoices for the lifetime of their relationship with Green-Got — so a 15-year customer can still reach their first invoices. This is a Green-Got product promise, not a statutory obligation: retaining full personal-data-bearing invoice bytes beyond the (a)/(b) minimums cannot be justified as “legal obligation forever” without legal review. It therefore requires its own: documented purpose; documented lawful basis (and DPIA/legal review where it bears on personal data); the §3.1 archive access contract controls; and a deletion / export / offboarding policy with customer controls (a customer who closes their account, or exercises rights over data outside the statutory window, must be handled by an explicit policy — not an open-ended “kept forever” default). document_vault governs integrity and access; its retention_until enforces (a)/(b) as the floor and the (c) policy thereafter — it is not an unbounded “never delete” flag for personal-data artefacts.

Permanent non-PII audit evidence (d). Permanent retention is cleanest for non-PII hashes and audit evidence — content hashes, the PAF integrity chain, normalised ids — which carry no full personal-data artefact and are far easier to justify keeping indefinitely than permanent full personal-data invoice bytes. Where Green-Got wants a permanent integrity proof, prefer hashes/evidence (d) over keeping the full PII-bearing artefact under an indefinite basis.

Invariant — distinguish the four bases; do not collapse them. “Permanent” is not a single blanket policy and the statutory periods are not mere “floors” under an indefinite legal-obligation basis for full PII-bearing artefacts. (a)/(b) are statutory legal-obligation retention; (c) is a separate product promise on its own basis and controls; (d) is permanent non-PII evidence. Operational logs, webhook payloads, and the personal data of contacts and signatories are governed by data-minimisation and CNIL-aligned operational retention (CNIL treats invoicing data as a 10-year intermediate-archive example), not by (c) or (d). See 13. Privacy and Data Protection for the full split between statutory retention, the product-archive promise, and operational / PII retention.

Design rule — store the legal artefact when it exists; regenerate only for display. Green-Got stores the exact legal artefact for every document class — the transmitted / PA-generated file for issued invoices, the original received file for incoming bills, the signed PDF for signed quotes. On-the-fly regeneration from DB data is a display convenience only (previews, fallback views); a regenerated file is not the legal archive, because renderers, validation rules, attachments, and legal output can change over time. See §5.

3. The Piste d’Audit Fiable (PAF)

A compliant electronic invoice must guarantee three properties from issuance to the end of the retention period: authenticity of origin, integrity of content, and legibility. The piste d’audit fiable is the method of demonstrating those properties by maintaining an unbroken evidence chain.

The PAF links each invoice to the underlying transaction it documents — the order, the delivery, the payment — so that an auditor can reconstruct the economic reality behind the invoice. To be “fiable” the chain must record:

Invariant: The audit trail must be unbroken. A gap in the chain — a missing actor, an unrecorded modification, a document whose integrity cannot be proven — invalidates the PAF for that invoice. Green-Got’s per-document evidence (its lifecycle status history, the transaction match, the content hash of the stored legal artefact, and the immutable structured invoice data) is the raw material of the PAF and must itself be retained for the full period.

Design rule — the PAF accompanies the stored legal artefact; it does not replace it. For issued invoices the stored legal artefact (the exact transmitted / PA-generated file, content-hashed — §5.2) is the authoritative archive. The reliable audit trail wraps it: the immutable structured invoice data, the lifecycle status history, the transaction match, the provider/audit metadata (request id, provider invoice id, tax report id, status history, validation output, retrieval timestamp), and the content hash together demonstrate authenticity, integrity, and legibility of that stored artefact. On-the-fly regeneration is a display convenience and is not part of the evidence chain — the legal proof rests on the stored bytes plus the audit metadata, all retained and versioned for the full period.

4. The Three Legal Methods

French law accepts three alternative methods for guaranteeing invoice authenticity, integrity, and legibility. A company chooses one (methods may be combined across document types).

MethodBasisHow integrity is guaranteed
QESeIDAS qualified electronic signatureA qualified signature on each invoice cryptographically proves origin and integrity. Heavy: every invoice must be signed and the signed bytes retained verbatim.
EDIStructured EDI exchange with controlsThe structured exchange plus a conforming audit trail and the prescribed EDI controls demonstrate the properties.
PAFReliable audit trail (piste d’audit fiable)The unbroken evidence chain (§3) — invoice ↔ order ↔ payment, with timestamps, actors, recorded changes, and integrity checks — demonstrates the properties without signing each invoice.

A company chooses one method (or combines them across document types). French/EU VAT law treats the three as equivalent ways to guarantee authenticity, integrity, and legibility.

Legal basis for the equivalence (PAF is a first-class method, not a fallback). BOFiP states the three methods explicitly and treats them as alternatives: a taxpayer using a QES (eIDAS qualified signature) is presumed to meet the authenticity/integrity/legibility conditions and is dispensed from documenting a PAF; a taxpayer using EDI under the prescribed controls is likewise covered; and a taxpayer using neither relies on controls establishing a reliable audit trail (piste d’audit fiable) linking the invoice to the underlying operation. The PAF is therefore the default legal route when no QES/EDI is used — not a weaker substitute. Source: BOFiP BOI-TVA-DECLA-30-20-30-10 (procédures de transmission par voie électronique) and BOI-TVA-DECLA-30-20-30-50 (contrôle des procédés d’authenticité/intégrité/lisibilité) — see §9 Sources.

Design rule — Green-Got standardizes on the PAF. Green-Got does not QES-sign every issued invoice and does not rely on classic EDI controls. Its method is the piste d’audit fiable: structured invoices are transmitted via a PA (B2Brouter), the exact transmitted / PA-generated legal artefact is fetched and stored (content-hashed — §5.2), and the persisted audit trail — the transmission record, the CDAR receipts, the AFNOR / internal status history, the matched transaction (order ↔ invoice ↔ payment), and the content hash of the stored artefact — together constitute the PAF over that stored file. The reliable audit trail (§3), not a per-invoice signature, is what guarantees the three properties for issued invoices.

Invariant: Authenticity / integrity / legibility for issued invoices rests on the PAF over the stored legal artefact, not on a QES and not on regeneration. Every element the PAF depends on (the stored legal artefact and its content hash, status history, CDAR events, transaction match, immutable structured data, provider/audit metadata) must be retained and kept unbroken for at least the statutory minimum (the longer of the 6-year fiscal / 10-year accounting window, §2), and — for the non-PII integrity evidence (content hash, PAF chain) — permanently (basis (d), §2). Availability beyond the statutory minimum for the full artefact is the product archive promise (basis (c)), governed by its own purpose, lawful basis, and customer/offboarding controls — not an indefinite legal obligation over PII-bearing bytes.

5. Storage Model — What Green-Got Keeps

The archiving design is governed by a single principle — for every invoice, store the exact legal artefact (the authoritative byte sequence) plus its content hash and audit metadata; regenerate from DB data only as a display convenience, never as the compliance archive. The principle applies uniformly to the three document classes Green-Got handles; what differs is which artefact is authoritative and where Green-Got fetches it.

Document classDirectionThe stored legal artefactWhere it livesRegeneration (display only)
Incoming bill (supplier invoice)INBOUND (AP, bills)The original received artefact (supplier’s Factur-X / UBL / CII), content-hashed — it cannot be regenerated by Green-GotStored in the core S3 bucket, bills/ subpart; document_ref points to itn/a — served directly from S3 by document_ref
Issued invoiceOUTBOUND (AR, invoicing)The exact transmitted / PA-generated file fetched from B2Brouter (download_legal_url), content-hashed, with provider/audit metadata — the authoritative legal archiveStored in the core S3 bucket alongside its immutable structured DB data and the PAFA preview/fallback view may be rendered on the fly from DB data, but it is not the legal copy delivered to an auditor
Signed quoteOUTBOUND (invoicing)The signed PDF + signature audit trail — a signature binds one specific rendered byte sequence; it cannot be regenerated identicallyStored as artefacts in the core S3 bucketn/a — served directly from S3 (the exact signed bytes)

5.1 Incoming bills — stored originals (no choice)

For inbound supplier invoices Green-Got must keep the original artefact: the piste d’audit fiable for a received invoice rests on the bytes the supplier actually sent, and Green-Got cannot reproduce them. The original is fetched via /invoices/{id}/as/original and retained in the core S3 bucket under the bills/ subpart for the full legal period (6 yr VAT / 10 yr accounting), and the inbound transmission / Bill carries a document_ref pointing to it. This resolves the document-storage-ownership question: the inbound original is retained in core S3 (bills/ subpart); the inbound event carries the document_ref, not the bytes. Any attachments[] on the received transmission are supplemental, never the legal original. See 10. Integration Contracts §4.

Invariant: An incoming bill’s original artefact is retained for at least the statutory minimum (6 yr VAT / 10 yr accounting, §2). Availability beyond that is the product archive promise (basis (c), §2) under its own purpose, lawful basis, and offboarding/deletion controls — not an indefinite legal obligation. document_vault governs integrity and access over the stored object and enforces the statutory floor then the (c) policy via retention_until.

5.2 Issued invoices — store the exact transmitted legal artefact

For outbound invoices Green-Got stores the exact final legal artefact that was transmitted — the PA-generated file fetched from B2Brouter via download_legal_url (the “final official file that was delivered”) — as the authoritative legal archive. A regenerated PDF / Factur-X is not the same legal artefact: renderers, validation rules, attachments, and legal output can change over time, so a later regeneration may differ in bytes, content, or layout from what was actually transmitted. BOFiP retention requires keeping electronic invoices in their original form and format with their full content and supporting audit evidence (BOI-CF-COM-10-10-30) — which a regenerated file cannot guarantee. Therefore the transmitted artefact is fetched and stored, not reconstructed.

This section is the single source of truth for the issued-invoice legal-artefact contract — other documents point here rather than redefining it. For every issued invoice Green-Got persists the following canonical fields; this list is authoritative.

FieldWhat it holdsWhy it is required
legal_artifact_refThe storage reference (core S3 key / document_vault handle) to the exact transmitted / PA-generated legal artefact, stored verbatimLocates the authoritative legal bytes — the file delivered to an auditor
content_hashCryptographic digest (e.g. SHA-256) of the stored artefact’s bytesProves the stored copy is intact and matches the provider’s record (integrity)
provider_invoice_idB2Brouter’s invoice id for the transmitted documentTies Green-Got’s record to the PA’s record; enables re-fetch and reconciliation
retrieval_timestampThe instant the legal artefact was fetched and hashedAnchors the content hash in time; provenance of the stored copy
legal_download_pathThe provider download path for the legal file — B2Brouter download_legal_url, served by Green-Got as /invoices/{id}/as/legalThe canonical retrieval path for the issued legal artefact
validation_versionThe validation ruleset version in effect at transmissionMakes the artefact’s provenance reproducible; lets a later display render be compared
rendering_versionThe renderer version in effect at transmissionSame — distinguishes the transmitted bytes from any later regeneration
audit_metadataProvider/audit metadata bundle: request id, tax report id, CDAR / AFNOR status history, validation outputThe PAF wrapper proving authenticity, integrity, legibility of the stored artefact

These canonical fields are persisted alongside the immutable structured invoice data (immutable on finalization) and the PAF (§3) wrapping all of the above.

Design rule — the legal file is the artefact behind legal_artifact_ref, never an attachment and never a regeneration. For an issued invoice the authoritative legal file is the one at legal_download_path (B2Brouter download_legal_url / /invoices/{id}/as/legal). For a received supplier invoice the authoritative legal file is the original the supplier sent, fetched via /invoices/{id}/as/original (§5.1). Any attachments[] on a transmission are supplemental material (annexes, extracted structured data, extra documents) — they are never the legal file and must not be substituted for it. Regeneration from DB data is preview/fallback only.

Design rule: Issued-invoice archiving = the stored exact transmitted legal artefact (legal_artifact_ref) + content_hash + provider_invoice_id + retrieval_timestamp + legal_download_path + validation_version + rendering_version + audit_metadata + immutable DB data + the PAF. On-the-fly regeneration is kept only as a display convenience (preview, fallback view) and is never the compliance archive. The legal copy delivered to an auditor is always the stored transmitted artefact, not a regeneration.

Invariant: The structured data backing an issued invoice is immutable once finalized (corrections are credit notes / avoir 381, never edits — see 4. Formats and Invoice Data). The stored legal artefact and its content hash are likewise never mutated: a corrected invoice produces a new artefact (a credit note / new invoice), never an in-place rewrite.

Invariant — provider dependency. Green-Got fetches the legal artefact from B2Brouter. If Green-Got relies on B2Brouter as a contractual archive of last resort, the retrieval SLA, export rights, exit plan, checksum strategy, and customer-access path must be documented and contracted (see §5.6). Green-Got’s own stored copy plus content hash is the primary archive; the provider copy is a redundancy, not a substitute for storing the artefact.

Green-Got’s chosen authenticity/integrity method is the PAF (§4), now anchored to the stored legal artefact rather than to regeneration. A certified SAE is not required — the maintained PAF over the stored, content-hashed artefact meets the authenticity/integrity/legibility requirements. See §5.5.

5.2.1 Fetch-and-store is a required post-send step in the durable workflow

Legal transmission of an issued invoice completes when the PA accepts the send — archiving does not gate transmission, and an invoice is not “untransmitted until archived”. But fetching the transmitted legal artefact (download_legal_url), hashing it, and persisting the §5.2 canonical field set is a mandatory post-send step, not a best-effort afterthought: without it there is no stored legal artefact, so the statutory-retention obligation (§2) and the PAF over a stored artefact (§4/§5.5) are unmet.

Design rule — wire fetch+hash+store into the same durable machinery as the send. The post-send fetch → content-hash → persist canonical fields + audit_metadata step runs under the existing retry / dead-letter mechanism of the outbound durable workflow (the send/status workflow described in b2brouter / 7. API Mechanics §10.3, which owns the retry/back-off/dead-letter policy). It is retried on failure (the legal artefact may not be immediately available at send time — poll/retry until download_legal_url resolves) and, if it ultimately fails, the transmission is flagged as archive-incomplete and dead-lettered for operator attention — never silently dropped. A transmission whose legal artefact was never fetched and stored is an archiving-incomplete anomaly, surfaced and resolved, not left dangling.

Invariant: Every accepted outbound transmission must reach a state where its stored legal artefact + content_hash exist, or be explicitly surfaced as archive-incomplete. The fetch+hash+store step is required and durable (retried under the workflow’s dead-letter machinery); it is post-send and does not alter the moment of legal transmission (PA acceptance).

5.3 Signed quotes — stored artefacts (exception)

A signed quote is the exception to the “generate on the fly” rule. A signature binds a specific rendered document (a specific byte sequence); re-rendering would produce a different file and break the signature. Therefore the signed PDF and its signature audit trail are retained as stored artefacts in the core S3 bucket and are never regenerated. Unsigned quote/invoice renders are generated on the fly like issued invoices.

5.4 The archiving service and where document_vault fits

Durable storage for the artefacts Green-Got keeps is Green-Got’s own archiving service — the core S3 bucket / “ggbs” archiving service (see Terminology). All three document classes store an artefact there: incoming bill originals, the transmitted legal artefacts of issued invoices (§5.2), and signed-quote artefacts. Durable storage is Green-Got’s service, not a third party’s.

document_vault sits on top of the archiving service: it is the retention-governance / coffre-fort over the stored artefacts — incoming bill originals, issued-invoice legal artefacts, and signed quotes — providing per-category retention_until, WORM tamper-evidence, audited share links, and the pluggable archival-provider abstraction (ObjectStorage/WORM → eIDAS-qualified certified archiver). It governs every stored legal artefact, including issued invoices.

Design rule: Every stored legal artefact (incoming bills, issued-invoice transmitted files, signed quotes) lives in Green-Got’s archiving service (ggbs / core S3), with document_vault governing its retention and the PAF (§3) wrapping it with the immutable DB data and audit metadata.

5.6 Relying on B2Brouter as a contractual archive

Green-Got’s primary archive of an issued invoice is its own stored copy of the transmitted artefact plus its content hash (§5.2). Where B2Brouter is also relied on as a contractual archive of last resort, the following must be documented and contracted:

Invariant: The provider copy is a redundancy, never a substitute for Green-Got storing the artefact. The legal obligation stays with the issuing company (§7); B2Brouter holding a copy does not transfer it.

5.6.1 Pre-launch contract-verification checklist (legal gate)

The terms above are not satisfied by Green-Got’s own storage design alone — several depend on what B2Brouter contractually commits to. Before enrolling any real customer or sending any real invoice, the following must be verified as in force in the executed B2Brouter contract (and the verification recorded). This is a launch-blocking legal gate, owned by legal review. The pass conditions per term are:

#Term to verifyPass condition
1Retrieval SLADocumented availability/latency for download_legal_url sufficient to meet the 30-day tax-authority obligation (§6).
2Export rightsContractual right to bulk-export every transmitted legal artefact + metadata at any time, in open formats.
3Exit planA defined repatriation procedure on relationship end, with no loss of legal evidence.
4Checksum strategyContent hashes recorded at retrieval, re-verifiable against the provider copy.
5Customer accessA path for the customer (and an auditor on their behalf) to reach the artefact independent of provider availability.
6CDAR / status-record retentionProvider-side retention of CDAR + AFNOR status records for ≥ the statutory window.
7PA-exit PAF-evidence transferContractual hand-over of the full PAF evidence set on termination / PA switch, in a re-importable form.
Recorded contract-verification table (closure record)

The pass-condition list above is not self-evidencing: a checklist that only states the conditions can be read as “satisfied” without any recorded proof. The table below is the recorded closure record — one row per §5.6 term, each carrying an evidence link, an owner, a verification date, and an explicit pass/fail status (default OPEN / unverified). It is the single place a launch reviewer checks to see whether the provider-archive gate is closed.

STATUS: OPEN — NOT CLOSED. Launch-blocking. None of the rows below has recorded evidence; every status is OPEN (unverified) and the evidence/owner/date cells are unfilled placeholders. Production B2Brouter configuration — and any regulated transmission for a real customer — is blocked until every row below reaches PASS with recorded evidence. This table is referenced as the closure home for the staging-matrix offboarding archive/export item (b2brouter / 8. Staging Verification Matrix F4).

#TermEvidence linkOwnerVerified (date)Status
1Retrieval SLA‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
2Export rights‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
3Exit plan‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
4Checksum strategy‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
5Customer access‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
6CDAR / status-record retention‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)
7PA-exit PAF-evidence transfer‹UNFILLED — link to contract clause / evidence›‹UNFILLED — legal owner›‹UNFILLED — YYYY-MM-DD›OPEN (unverified)

Gate. Production configuration is blocked until all seven rows are PASS with recorded evidence, owner, and date. While any row is OPEN (unverified) the provider-archive gate is not closed and the no-real-traffic rule below applies in full.

Invariant: No regulated transmission for a real customer occurs until every row above is verified and recorded as PASS. An unverified provider-archive assumption is treated as not satisfied.

5.5 Certified SAE — Not Required (PAF Is Our Method)

A certified SAE (NF Z42-013 / NF Z42-029) gives the strongest evidentiary presumption that a stored artefact is intact and untampered. Green-Got’s decision is to rely on the PAF (§3/§4) plus durable archiving with WORM tamper-evidence on stored artefacts — this meets the law’s authenticity / integrity / legibility requirements without a certified SAE.

Legal basis — the PAF and a certified SAE are distinct things. The PAF is the BOFiP authenticity/integrity/legibility method (§4, BOI-TVA-DECLA-30-20-30-10/-50): it answers “is this invoice genuine?”. A certified SAE conforming to NF Z42-013 (= ISO 14641-1) / NF Z42-029 answers a different question — “can the stored copy be presented as probative-value evidence in a dispute?” — by guaranteeing integrity, traceability, and an immutable chain of custody (digital fingerprint / SHA-256, sealing, journaling). French case law treats NF Z42-013 conformity (or NF 461 certification) as strong support for admissibility, but it is a voluntary standard, not a regulatory precondition for VAT-compliant archiving: the law requires the invoice be retained in its original form/content with audit evidence (BOI-CF-COM-10-10-30), which the maintained PAF over the stored, content-hashed artefact satisfies. Sources: AFNOR NF Z42-013 / ISO 14641-1, §9 Sources.

Design rule: A certified SAE is not part of the design. It remains available as an optional future enhancement (e.g. to obtain the strongest evidentiary presumption for a specific dispute), but the launch architecture stands on the maintained PAF over the stored, content-hashed legal artefact.

Launch gate — PAF authenticity rests on B2Brouter retaining the CDAR / status evidence we cannot regenerate. Because Green-Got’s authenticity method is the PAF (not a per-invoice QES), the CDAR receipts and the AFNOR lifecycle-status history are load-bearing PAF evidence — and for outbound invoices that evidence originates with B2Brouter (the PA). Green-Got fetches and persists this evidence into the permanent audit_metadata bundle (§5.2), but the B2Brouter contract (§5.6) must additionally guarantee provider-side retention of CDAR / status records for at least the statutory window, so the PAF can be reconstructed or re-verified even after a status record ages out of Green-Got’s hot path. Verifying this guarantee is contracted is a pre-launch legal gate (see the §5.6 checklist).

5.7 Provider-exit export package (launch-grade, rehearsed before first customer)

The product archive promise (basis (c), §2) — and the statutory retention beneath it — is only credible if Green-Got can prove it can move every artefact and its evidence chain out — on a PA switch, a provider failure, or a forced repatriation — without losing legal evidence. This requires a minimal export package that is defined and rehearsed end-to-end (export → restore) BEFORE enrolling any real customer, not improvised at exit time.

The export package must contain, for the enrolled scope:

Invariant — the export must be TESTED before the first real transmission. The export → restore round-trip is rehearsed and verified (hashes re-validate, artefacts re-open, the evidence chain reconstructs) before Green-Got enrolls any real customer or sends any real invoice. An untested export plan does not satisfy the statutory-retention obligation or the product archive promise (§2). This package is the concrete substance behind the §5.6 export rights and exit plan terms, and behind the PA-switch / offboarding obligations in 3. Actors and Legal Posture §5. The exact PA-switch handover format demanded by the final decree is still [open — legal/provider-support]; the package above is the launch-grade minimum Green-Got controls regardless.

5.8 Archive access contract

How the archive is reached — roles, tenant isolation, customer self-service, support limits, auditor share links, break-glass, download limits, immutable access logs, periodic review — is specified once as the archive access contract in 13. Privacy and Data Protection §3.1. Retrieval through document_vault (search + per-token audited share links, §6) operates under that contract.

5.9 Product Archive Promise — Retention and Offboarding Policy

This section makes basis (c) (§2) operative. Retention basis (c) — availability of the full legal artefact beyond the (a)/(b) statutory minimums — is a product promise on its own purpose and lawful basis, not an extension of the legal-obligation basis. Until this policy is defined and legally reviewed, basis (c) is an undocumented lawful basis, which is not a lawful basis (GDPR Art. 5(1)(e) storage limitation, Art. 5(2) accountability). This is therefore a legal-review launch gate.

Chosen product decision (per uncertainties.md L-5). Green-Got’s product promise is indefinite retention of the customer’s legal artefacts for the lifetime of the relationship (“always retrievable” — a 15-year customer can still reach their first invoices), with a full export bundle delivered on offboarding. The policy below frames that decision while supplying the controls a “kept beyond statutory” basis legally requires.

Retention horizon. For the duration of the customer relationship, legal artefacts are retained indefinitely under basis (c). The clock that matters is the (a)/(b) statutory floor (the longer of 6 yr VAT / 10 yr accounting, §2): below that floor, deletion is prohibited (legal obligation) regardless of customer wishes; above it, retention is the (c) product promise and is subject to the customer-exit and DSAR mechanics below.

Two offboarding paths — provider-exit vs customer-exit (distinct).

Customer-exit flow (account closure / unsubscribe).

  1. Export window. On account closure the customer is offered (and, on request, delivered) a full export bundle of their legal artefacts and evidence chain — the same content set as the §5.7 package, scoped to that one customer — in open, re-usable formats, within a defined window (the export-availability SLA is a launch-gate parameter set with legal/product).
  2. Statutory floor is preserved. Artefacts still inside the (a)/(b) statutory window are moved to restricted-access intermediate archive and retained for the remainder of that window even after closure (legal obligation overrides erasure — see 13. Privacy §5). They are not deleted at closure.
  3. Post-statutory deletion. Artefacts past the statutory floor are no longer compelled by basis (a)/(b). Because basis (c) is a product promise (not a legal obligation), at customer-exit the default is deletion of the post-statutory PII-bearing artefact unless the customer affirmatively asks to keep it; the non-PII integrity evidence (hashes, PAF chain — basis (d)) is retained regardless.

Post-statutory DSAR / right-to-erasure mechanics. While an artefact is inside the statutory window, an erasure request is answered with the legal-obligation exception (Art. 17(3)(b)). Once the statutory window has elapsed, that exception no longer applies and basis (c) does not by itself defeat an erasure request: a post-statutory erasure or account-closure request is honoured by deleting the PII-bearing artefact while retaining the non-PII integrity evidence (basis (d)). The full split is specified in 13. Privacy §5.

Two-stage retention is a data-model requirement, not just prose. A single “delete after N days” / retention_until field cannot express this policy, because the lifecycle has two stages with different rules: (1) a statutory floor (a)/(b) below which deletion is prohibited, then (2) a product-promise extension (c) above which deletion is policy-driven (default-delete on customer-exit, indefinite-keep while active and opted-in). document_vault must model both stages — see document_vault/plan.md → RetentionPolicy. The statutory floor and the product-promise horizon are separate fields; the deletion decision reads both plus the customer’s offboarding/opt state.

Invariant — basis (c) is gated on this policy + legal review. No PII-bearing artefact is retained beyond the statutory floor under an open-ended “kept forever” default. Retention beyond the floor requires: this documented policy, a documented lawful basis (and DPIA/legal review where it bears on personal data), the §3.1 archive access contract controls, and the customer-exit export/deletion flow above. Defining and legally reviewing this policy is a launch gate.

6. Accessibility to Tax Authorities

Invoices must be accessible to the tax authority on request, within approximately 30 days. Retrieval must support lookup by the relevant identifiers (counterparty, number, date, amount) and deliver the stored legal artefact together with the evidence proving its integrity. For every document class — incoming bills, issued invoices, and signed quotes — the artefact served is the stored legal file (content-hashed), retrieved through document_vault’s search (category, date range, counterparty name, amount) and per-token audited share links, delivered alongside the PAF (status history, transaction match, content hash, provider/audit metadata) that proves its authenticity and integrity. On-the-fly regeneration is never used to satisfy an auditor request; it is a display convenience only.

6.1 Tax-authority access protocol (distinct from auditor / customer paths)

A tax-authority (DGFiP) access request is not the same path as a customer’s self-service access or a private auditor’s share link, and must not be conflated with them:

  1. Bounded scope. The request is scoped to a specific customer/tenant, identifier set (counterparty, invoice number, date range, amount), and period; cross-tenant reach is structurally impossible (§3.1 tenant isolation).
  2. Deliverable. For each in-scope document: the stored legal artefact (content-hashed) plus its PAF (status history, transaction match, content_hash, provider/audit metadata) — never a regeneration.
  3. 30-day SLA. Assembly and delivery must complete within the ~30-day accessibility obligation; the §5.6 retrieval SLA (and provider-side CDAR retention) must be sufficient to meet it.
  4. Audit + authorisation. The access is performed under an explicit, logged authorisation (the §3.1 break-glass workflow with mandatory reason capture, or a dedicated tax-authority scope), and every artefact read / share-link mint is written to the immutable access log.

document_vault feature confirmation. This protocol is served by features document_vault already provides: search (category, date range, counterparty name, amount), per-token audited share links with TTL + revocation, and the break-glass elevated-access path with mandatory reason capture and immutable logging (13. Privacy §3.1). No new retrieval primitive is required — a tax-authority request is a scope/authorisation profile over the existing search + share-link + break-glass + audit-log surface, not a separate store.

7. Residual Responsibility

The company remains legally responsible for the compliance of its archiving even when a platform or third-party archiver holds the documents on its behalf. Delegating storage to B2Brouter (as a PA) or to a certified SAE provider does not transfer the legal obligation away from the company.

Invariant: Liability for archiving compliance stays with the issuing company. The PA is responsible for secure transmission, format compliance, and its own audit trail; it is not a substitute for the company’s archiving obligation. See 3. Actors and Legal Posture.

8. Related Documents

9. Sources

9. Mandate and Onboarding

Mandate and Onboarding

This document describes how a Green-Got business customer is enrolled on the approved platform (B2Brouter) so that they can issue and receive e-invoices: the in-app mandate the customer grants, the white-label account creation through eDocSync, the activation of DGFiP/PPF Annuaire registration, the declaration of routing scope, and what Green-Got persists.

1. Terminology

Terms shared across the documentation set are defined in 1. Reform Overview. This document adds:

2. Overview

Enrollment is the prerequisite for every regulated flow. A Green-Got customer cannot issue an e-invoice (Flux 1), receive a supplier e-invoice, or e-report (Flux 10) until they are an Annuaire-registered company with a chosen PA. Because B2Brouter is Green-Got’s PA, enrollment means: create the customer’s B2Brouter account, capture the mandat, and enable the dgfip tax report settings (which registers them in the Annuaire at SIREN-level routing scope — the only scope supported at MVP).

Design rule: Green-Got is a Solution Compatible (SC), not an approved platform. It never registers a company in the Annuaire directly; registration is a side effect of enabling B2Brouter’s dgfip tax report settings. All regulated transport and reporting pass through B2Brouter. See 1. Reform Overview §5 and 3. Actors and Legal Posture.

Design rule: Enrollment is organisation-scoped, not invoice-scoped. One enrollment exists per French legal entity (per SIREN). Every invoice that entity issues or receives reuses the same B2Brouter account; the b2brouter_account_id is read from the organisation’s enrollment, never recreated per invoice.

Invariant: Reception is universal from 2026-09-01 — every in-scope French assujetti (every Annuaire-registered entity) must be enrolled and Annuaire-registered by that date, regardless of size, because any large/ETI supplier may send them an e-invoice. Issuance obligations are phased (see 1. Reform Overview §4). Green-Got treats Annuaire registration as the binding milestone for all customers.

3. Terminology of the journey: the four steps

Enrollment is composed of four ordered steps. Each is described in detail in §4.

StepNameActor that drives itWhat it producesLegal/technical effect
(a)Mandate UICustomer (consents) in the Green-Got appCaptured, timestamped Green-Got-owned mandate consent record (B2Brouter has no mandate object)Legal authorization for Green-Got/B2Brouter to act on the company’s behalf
(b)B2Brouter account creationGreen-Got plateforme_agreee via eDocSync (POST /accounts)b2brouter_account_id (one per SIREN)The company exists as a sender+receiver in B2Brouter
(c)Enabling DGFiPGreen-Got plateforme_agreee (POST …/tax_report_settings to create; PUT …/tax_report_settings/dgfip to update)dgfip setting created (full body)Triggers PPF Annuaire registration; enables Flux 1 + Flux 10
(d)Routing scope (SIREN-only at MVP)Green-Got (automatic; no customer choice)SIREN-level Annuaire routing entry (0225:SIREN)The company is discoverable and addressable; SIRET / internal-code scope DISABLED (gated behind P-2)

4. The Enrollment Journey

4.1 Step (a) — The in-app mandate UI

Before any call to B2Brouter, the customer must explicitly grant the mandat. This is the legal foundation of the whole enrollment: it is what authorizes Green-Got (as SC) and B2Brouter (as PA) to act on the company’s behalf for regulated flows.

What the customer agrees to. The mandate UI presents the contract/consent the customer must accept. It captures, at minimum:

What Green-Got captures — the Green-Got-owned mandate consent record. Because B2Brouter exposes no explicit mandate object (its authorization is account-level — owner/admin roles), Green-Got is the system of record for the mandate. Green-Got persists an organisation-level mandate consent record:

FieldMeaning
organisation_idThe granting company (one mandate per French legal entity / SIREN).
scopeWhat is authorized: issue, receive, e-report (Flux 10), and manage lifecycle statuses on the company’s behalf.
accepted_atConsent timestamp.
consent_versionThe exact contract/consent text + version shown to the customer (so the precise wording accepted is reproducible).
accepted_byThe natural person who consented (authenticated user id).
signatory_roleThe role under which that person bound the company (e.g. legal representative, authorised signatory) — the claimed authority, captured at consent.
kyb_reference_idA reference to the exact KYB record/version that backed the signatory’s authority at capture time, so the authority is reproducible even if KYB is later updated.
kyb_verified_atWhen that KYB authority was verified, for auditability of the capture moment.
statusactiverevoked / superseded (on revocation or PA switch).

This record is part of the reliable audit trail (PAF) and is the evidence that the downstream B2Brouter account and Annuaire registration were authorized. Legally, the company stays responsible for invoice content and compliance; the mandate authorises transmission on its behalf by Green-Got (SC) + B2Brouter (PA).

The consent record above captures that consent was given. The full audit-ready evidence bundle — signed designation artifact, contract text + hash, signatory authority, capture metadata, scope flags, registration/account proofs, and the revocation trail — is the MandateEvidence contract in §4.5, and it is MandateEvidence (not the subscription) that gates the regulated flows.

Design rule — clear in-app consent before any submission. The mandate is captured through a clear, explicit in-app consent step during onboarding, and it is captured before the B2Brouter account is created and before any invoice is submitted on the customer’s behalf. Account creation and DGFiP enablement are the technical execution of a consent that must already exist. An enrollment must never reach Annuaire registration — and Green-Got must never transmit on the customer’s behalf — without an active mandate consent record.

Design rule — Green-Got is the mandate system of record. Since B2Brouter has no mandate object, the Green-Got-owned consent record is the authoritative mandate. No platform-side mandate registration is required or available; the authorization B2Brouter understands is the account-level provisioning Green-Got performs under eDocSync, which is itself authorized by this record.

Edge case — mandate revocation / re-consent. The mandate is revocable. If the contract text/version changes materially, or the customer revokes and re-grants, a new consent record is captured and the prior one is marked superseded; the latest active consent governs. Revocation implies winding down the enrollment, and a PA switch marks the record revoked/superseded (see §5.4 switching PA).

Design rule — revoked vs superseded is deterministic, set by the triggering actor. The two terminal-from-active states are not interchangeable; exactly one applies per transition:

Rule of thumb: replaced by a new active record → superseded; ended without a successor → revoked. A PA switch that only moves inbound designation does not touch the consent status at all (it is a routing change, not a mandate change — §5.5); only a withdrawal of the issuing mandate sets revoked.

In-flight outbound on revocation. When the issuing mandate is revoked, Green-Got stops accepting new outbound submissions for that organisation immediately (the gate in §4.5 fails). A transmission already submitted to B2Brouter is not retracted — it was authorized by the then-active mandate, the legal act is complete, and its status continues to be tracked to completion under the consent version in force at submission. A transmission drafted but not yet submitted is blocked at the gate. superseded (re-consent) never blocks in-flight outbound, because authorization is continuous.

4.2 Step (b) — B2Brouter account creation via eDocSync

Once the mandate is captured, Green-Got provisions the customer in B2Brouter using the eDocSync white-label model. This is a single REST call.

Invariant — two separate immutability concerns. (1) B2Brouter documents the TIN (FR VAT number, tin_*) as non-updatable after account creation. (2) The CIN (SIREN/SIRET legal identity, cin_*) carries the routing/legal identifier; Green-Got treats a CIN change (e.g. a wrong SIREN that must be corrected) as a KYB edge case requiring controlled reprovisioning — not a routine PUT update — unless staging proves safe PUT /accounts/{account} update semantics for the CIN. Either way, a SIREN correction is not a routine field update; it requires the edge-case handling in §5.

Design rule: Green-Got sources the account fields from the organisation’s KYB record (legal name, SIREN/SIRET, registered address). The B2Brouter account is a projection of Green-Got’s authoritative organisation data, not an independent source of truth.

4.3 Step (c) — Enabling DGFiP (PPF Annuaire registration)

Creating the account makes the company exist in B2Brouter but does not yet register it with the French State. Registration is triggered by enabling the dgfip tax report settings.

Design rule: Annuaire registration is a side effect of enabling dgfip, performed by B2Brouter — Green-Got never calls the PPF or Annuaire directly. After this step the company is discoverable by other approved platforms and can both send and receive.

Design rule — tax_report_settings update (PUT …/dgfip) is a settings update, not a fresh Annuaire registration. The initial POST is what creates the registration (the one-time Annuaire side effect). A subsequent PUT …/dgfip updates the existing tax-report-setting body and does not re-trigger a new Annuaire registration on an already-registered, unchanged routing identity — this resolves the apparent contradiction with the §5.2 “re-enabling dgfip is a no-op” rule: re-enabling an already-enabled setting is idempotent (no-op); updating the setting body propagates the changed fields to the existing registration. Whether changes to descriptive fields carried in the body (e.g. naf_code, enterprise size) cause B2Brouter to re-propagate / refresh the Annuaire entry — versus the routing identity (SIREN + 0225 scheme), which alone determines addressability and is not changed by these fields — is not documented and must be confirmed on staging; it extends uncertainty P-1. [open — provider support / staging-wire] (does a naf_code / enterprise-size PUT re-register or refresh the Annuaire entry, or is it a silent settings update?).

Testing note. The DGFiP qualification environment (QAS) disallows real SIREN/SIRET. Enrollment tests must use the fictitious Chorus Pro QAS identifiers, not production company numbers. See B2Brouter — Onboarding accounts.

4.4 Step (d) — Declaring routing scope

The reform requires every company to be addressed in the Annuaire, before 2026-09-01, at one of three granularities. The Annuaire address space supports all three; Green-Got’s MVP routing scope is SIREN-only (see the design rule below):

See 6. Annuaire and Routing §3 for the routing key semantics.

Design rule — MVP routing scope is SIREN-only; SIRET / internal routing are DISABLED future states. B2Brouter publishes no dedicated API for choosing routing-scope granularity, so Green-Got does not expose a scope choice at MVP. Green-Got declares SIREN-level (0225:SIREN) only, automatically, as part of enabling the dgfip tax-report setting:

Because the scope is fixed to SIREN at MVP, it is applied automatically by the dgfip enablement that drives the Annuaire registration; Green-Got never writes the Annuaire directly, and there is no separate “declare scope” step the customer must complete for SIREN-level enrollment.

Design rule — non-SIREN routing scope is UNSUPPORTED until the provider mechanism is confirmed (P-2). SIREN-level (0225:SIREN) matches B2Brouter’s one-account-per-SIREN model, so it works without any extra provider call. SIRET-level and internal-code scope require an as-yet-undocumented B2Brouter endpoint/field to convey the chosen granularity at account-configuration time. B2Brouter publishes no documented endpoint or field for selecting routing-scope granularity, and we have not confirmed one exists. Until the exact endpoint/field is documented and confirmed on staging, Green-Got treats SIRET-level and internal-code routing scope as DISABLED / UNSUPPORTED and ships SIREN-level only. We do not expose a UI option whose backing provider call is unverified.

Note — Green-Got-authored design; provider mechanism unconfirmed (P-2). The Annuaire address space supports SIRET / internal-code scope, but the exact endpoint/field by which Green-Got would convey it to B2Brouter at account-configuration time is not documented and not yet confirmed — uncertainty P-2. [open — provider support] (does a routing-scope-granularity endpoint/field exist, and what is it?) and [open — staging-wire] (confirm the exact call and its effect on the Annuaire entry on staging before enabling non-SIREN scope). Non-SIREN scope stays DISABLED and SIREN-only until both are closed.

4.5 MandateEvidence — the audit-ready evidence contract

The mandate consent record in §4.1 captures that the customer consented. It is not, on its own, audit-ready: a tax/DGFiP audit (or a legal challenge to “did Green-Got have authority to transmit on this company’s behalf?”) requires the full chain of proof — the signed designation, the exact wording accepted, who had authority to bind the company, how and from where consent was captured, and proof that the downstream registrations were made under that authority. MandateEvidence is the Green-Got-owned contract that documents this evidence bundle as a first-class model.

Design rule — MandateEvidence is the audit-ready superset of the consent record. MandateEvidence wraps the §4.1 consent record and adds the surrounding proof artifacts. It is organisation-scoped (one per French legal entity / SIREN), append-only (superseded versions are retained, never overwritten), and part of the reliable audit trail (PAF).

FieldMeaning
designation_artifactThe signed mandat de désignation document itself, or a stable reference to it (storage key / signed-document id). The artifact the customer actually signed, not a description of it.
contract_text_version + contract_text_hashThe exact contract/consent text version shown and a content hash of that text, so the precise wording accepted is both reproducible and tamper-evident.
signatory_authorityEvidence that the consenting natural person had authority to bind the company — tied to the organisation’s KYB record (role, representative status, KYB reference).
consent_captureThe capture metadata: accepted_at timestamp, source IP, session id, device id, and authenticated user id. Establishes when, from where, and by whom consent was given.
scope_flagsExplicit per-capability authorization flags: issue, receive, e-report (Flux 10), payment-data, archive, support. Each regulated flow is gated on its corresponding flag (see the gating rule below).
annuaire_registration_proofProof that PPF Annuaire registration was performed under this mandate (the DGFiP enablement result / registration confirmation returned via B2Brouter).
b2brouter_account_proofProof of the B2Brouter account and its tax-setting (dgfip) state created/enabled under this mandate (account id snapshot + tax-report-setting result).
revocation_trailThe revocation / supersession history: each state transition (activerevoked / superseded), with timestamp, actor, and the id of the superseding MandateEvidence record. Never deleted.
retention_export_policyThe retention period applied to this evidence and the export policy (format + on-request export path) for audit and PA-switch portability. See 8. Archiving §5.

Design rule — regulated flows are gated on explicit mandate/designation states, not on subscription. A live business-account subscription is a billing fact; it is not a legal authorization. The legal basis for acting on a customer’s behalf is the mandate (MandateEvidence with the relevant scope_flags set and status = active) and, for inbound, being the designated PA in the Annuaire. Each regulated flow is gated independently:

FlowRequired state to proceed
Outbound issuance (Flux 1 send)MandateEvidence status = active with scope_flags.issue AND an active business-account subscription. (Mandate is the legal basis; subscription is the commercial precondition. Subscription alone is not sufficient.)
Inbound receptionMandateEvidence status = active with scope_flags.receive AND Green-Got is the designated PA for that SIREN in the Annuaire (§5.5).
E-reporting (Flux 10)MandateEvidence status = active with scope_flags.e_report. This includes AP acquisition reporting — the ReportableAcquisitionRecorded overlay (CGI art. 290 acquisition data) is gated on scope_flags.e_report, not on a separate flag, because it is Flux 10 reporting of a received acquisition (it is not payment data, so payment_data does not gate it). See 12. Data Model and 10. Integration Contracts §6.
Payment-data submissionMandateEvidence status = active with scope_flags.payment_data. This is the seller-side VAT-on-collection Flux 10 payment data only; it is separate from e_report and the two are never merged.

Design rule — scope_flags lifecycle: defaults, capture, and per-flag revoke/enable. The flags are not granular opt-in at capture; they are derived from a single bundled consent and may later be narrowed.

Launch-blocking — mandate wording and capture must be confirmed. The exact legal wording of the mandat de désignation, and the precise capture mechanism (what must be signed and how), are not yet confirmed. Until legal confirms the contract wording and provider support confirms how the designation is recorded against the B2Brouter account, MandateEvidence cannot be treated as complete and issuance/inbound/e-reporting must not go live. This is a launch blocker, not a polish item. [open — legal] (contract wording / capture form) and [open — provider support] (how the designation is recorded against the B2Brouter account and how its state is read back).

5. Idempotency and Edge Cases

5.1 Account already exists for a SIREN

Because B2Brouter enforces one account per SIREN, a second POST /accounts for the same SIREN returns a 422 with a taken condition rather than creating a duplicate.

Design rule: Enrollment is idempotent on SIREN. Before creating an account, Green-Got resolves the existing account by querying GET /accounts filtered on cin_value (scheme 0002 SIREN — the legal-identity/company number, not tin_value, which carries the FR VAT number). If an account already exists, Green-Got adopts it — persisting the returned b2brouter_account_id on the organisation — rather than creating a new one. Re-running enrollment must converge to the same account, not error out or duplicate.

5.2 Re-enrollment / retries

A partial enrollment (e.g. account created but dgfip not yet enabled, or process interrupted between steps) must be safely resumable. Green-Got models enrollment as a state machine over the four steps and, on retry, advances from the first incomplete step:

stateDiagram-v2
    [*] --> MandateCaptured: customer consents
    MandateCaptured --> AccountCreated: POST /accounts (or adopt existing)
    AccountCreated --> DgfipEnabled: POST tax_report_settings (create; PUT .../dgfip to update)
    DgfipEnabled --> RoutingDeclared: SIREN-level scope confirmed (automatic; SIRET/internal DISABLED, P-2)
    RoutingDeclared --> Enrolled
    Enrolled --> [*]

    AccountCreated --> AccountCreated: idempotent retry (adopt existing)
    DgfipEnabled --> DgfipEnabled: enabling dgfip is idempotent

    MandateCaptured --> KybBlocked: signatory-authority / KYB check fails (§4.1)
    AccountCreated --> AccountFailed: POST /accounts hard failure (non-`taken`)
    DgfipEnabled --> DgfipEnablementFailed: tax_report_settings 5xx / DGFiP-PPF enablement failure

    KybBlocked --> [*]: terminal (manual review)
    AccountFailed --> AccountCreated: retry while retry_attempt < max
    AccountFailed --> [*]: terminal after max retries
    DgfipEnablementFailed --> DgfipEnabled: retry while retry_attempt < max
    DgfipEnablementFailed --> [*]: terminal after max retries

Invariant: Each happy-path step is idempotent. Re-creating the account adopts the existing one (§5.1); re-enabling dgfip on an already-enabled account is a no-op; re-confirming the (SIREN-only) routing scope is a no-op. Enrollment is safe to re-run end-to-end at any time.

Failure states — distinct from the handled idempotent paths. The taken condition (§5.1) and the wrong-SIREN reprovision (§5.3) are handled branches, not failures. The state machine additionally carries explicit failure states, each with a per-state retry-vs-terminal policy:

Failure stateTriggered byClassPolicy
KybBlockedThe signatory-authority / KYB check (§4.1) cannot establish that the consenting person may bind the company.Terminal (until KYB is re-resolved out-of-band).Do not auto-retry. Park the enrollment, surface a customer/ops alert, resume only after KYB clears. No B2Brouter call is made (the mandate is not yet valid).
AccountFailedPOST /accounts returns a hard failure that is not the handled 422 taken (e.g. 400 invalid params after exhausting correction, 401 auth, 5xx, network/timeout).Transient for 5xx/network/timeout; terminal for deterministic 400/401 after correction.Retry transient failures with backoff while retry_attempt < max_retry; on retry_attempt == max_retry move to a terminal failure with an ops alert. A deterministic 400/401 is terminal immediately (no blind retry).
DgfipEnablementFailedPOST/PUT …/tax_report_settings returns 5xx, or the DGFiP/PPF enablement otherwise fails after the account exists.Transient (5xx/network/timeout).Retry with backoff while retry_attempt < max_retry; on exhaustion move to a terminal failure with an ops alert. Because the account already exists, retry re-enables dgfip idempotently.

Uncertainty. B2Brouter does not document explicit idempotency keys for POST /accounts. Green-Got’s idempotency relies on the SIREN-uniqueness invariant and a pre-create lookup rather than a B2Brouter-provided idempotency key. Tracked in uncertainties.md.

5.3 Re-enabling after a correction

A wrong SIREN is a CIN (cin_*) legal-identity change, which Green-Got treats as a KYB edge case requiring controlled reprovisioning rather than a routine account update (see §4.2). The correction path is out-of-band (delete/re-provision via B2Brouter support or a new account for the correct SIREN), after which Green-Got re-points the organisation’s b2brouter_account_id. This is an exceptional, manually-reviewed operation, never a routine flow. (Separately, B2Brouter documents the TIN — the FR VAT number, tin_* — as non-updatable; that is a distinct field from the CIN and not the SIREN.)

5.3b Resuming a halted enrollment (reactivation, re-adopt, re-enable)

An enrollment may halt mid-journey — parked in a failure state (§5.2), or interrupted between steps. Resuming it must converge to a consistent account state, never blindly re-run from the top.

Design rule — pre-flight status check before resuming. Before resuming a parked/halted enrollment, Green-Got issues a pre-flight GET /accounts/{account} (when a b2brouter_account_id is already persisted) to read the account’s current state, plus the existing GET /accounts?cin_value=SIREN lookup (§5.1) when no id is held yet. Resumption is then deterministic:

Re-adoption uses the same SIREN-idempotent path as a fresh run, so a resume never duplicates an account. The pre-flight check guarantees the resume advances from the true provider state rather than a possibly-stale local state-machine position.

5.4 Switching PA (off-boarding and on-boarding from another PA)

A customer may change approved platform in either direction — leave B2Brouter for another PA, or arrive at Green-Got/B2Brouter from another PA. The reform makes this mostly automatic and driven by the new (gaining) PA, with minimal action required on the losing side.

How a PA switch works (reform rules).

Design rule — leaving Green-Got: minimal losing-side work. If a customer leaves Green-Got (so B2Brouter is the old PA), Green-Got’s active work is minimal. B2Brouter, as the PA, carries the acknowledge / 12-month-continuity / 10-year-archive obligations. Green-Got’s responsibilities are limited to: mark the mandate consent record revoked/superseded, stop acting on the customer’s behalf, and preserve the audit trail and stored artefacts for their retention period (see 8. Archiving §5). Green-Got never silently drops an enrollment, and it does not need to perform a manual Annuaire de-registration — the gaining PA overwrites the entry.

Design rule — joining Green-Got from another PA. If a customer arrives from another PA, the switch is driven by the normal B2Brouter onboarding path: account creation under eDocSync and enabling the DGFiP tax_report_settings (which updates the Annuaire routing entry to B2Brouter — see §4.3). The new mandate consent record is captured first (§4.1), exactly as for a fresh enrollment. Reversibility is ensured contractually so customers are never locked in.

Invariant: A PA switch is a routing/mandate change, not a data migration Green-Got must orchestrate on the losing side. The Annuaire guarantees identifier portability; the losing PA’s continuity and archive obligations are the PA’s, not the SC’s.

5.5 Detecting “we are no longer the PA”

Because the Annuaire is last-registration-wins, a customer can move to another PA at any time without telling us. There is no documented push notification (“you have lost designation”) from the PPF or B2Brouter. Detection is therefore Green-Got’s responsibility — primarily so we can tell the customer that their inbound flux has moved (we will no longer receive their supplier invoices), since their ability to send through Green-Got is unaffected so long as their issuing mandate is still valid and their subscription is active.

Two distinct things — do not conflate them. Losing inbound designation affects routing, not the mandate:

These are independent. When the customer’s Annuaire entry is overwritten to point at another PA, we lose inbound designation — but that does not revoke the issuing mandate. The mandate is only revoked through the explicit revocation/supersession path (§4.1, §5.4). So:

Design rule — inbound stops; outbound continues while the issuing mandate is valid AND the subscription is active. Designating another PA stops only the inbound flux for that SIREN (we stop receiving their supplier invoices); it does not stop outbound issuance through Green-Got and does not revoke the issuing mandate. Outbound issuance is gated on two explicit states — a valid issuing mandate (scope_flags.issue, status = active) and an active business-account subscription — never on subscription alone. There is no minimum-service / continuity window on our side: Green-Got owes no fixed-duration continuity for a customer that moves to another PA. (The losing-PA continuity obligation in §5.4 is B2Brouter’s as the PA, a separate scenario.)

How loss of designation surfaces:

Note — internal error model. The internal error model (see 5. Lifecycle Statuses §1.1) carries a dedicated not_designated_pa variant. The exact B2Brouter/Annuaire error code for “this account is no longer the designated PA” is not documented and is a confirm-on-staging item (see Uncertainties).

Design rule — the detection sweep is operational, not best-effort. The proactive Annuaire/directory lookup runs as a scheduled designation sweep over all active enrollments:

Design rule — the stand-down process. On detecting loss of designation, Green-Got:

  1. records that inbound has moved away (the customer’s identifier now resolves to another PA), timestamped — persisted as inbound_designation_lost_at on the enrollment (see 12. Data Model);
  2. stops expecting inbound for that SIREN — supplier invoices will no longer arrive through Green-Got;
  3. keeps outbound issuance available for as long as the issuing mandate is valid (MandateEvidence status = active with scope_flags.issue) and the business-account subscription is active — it is gated on the mandate and the subscription, not on inbound PA-designation;
  4. retains archives + audit trail for the full retention period (see 8. Archiving §5);
  5. proactively alerts the customer (see the Design rule below).

Design rule — the customer alert on loss of designation is mandatory, not best-effort. Because there is no push notification from the PPF/B2Brouter and the switch can happen without the customer telling us, on detecting loss of designation Green-Got must proactively alert the customer through both in-app and email channels. The alert must state, clearly: (1) that their PA designation changed (another platform is now their designated PA); (2) that they will no longer receive supplier invoices through Green-Got (incoming bills for that SIREN now route to the other PA); and (3) what the customer needs to do (confirm the switch was intended, or re-designate Green-Got if it was not). It should reassure them that they can still send invoices through Green-Got while their issuing mandate is valid and their business-account subscription is active. This alert is not optional and not best-effort — an undetected, silently-handled loss of designation that leaves the customer unaware is a defect: the customer must always be told, because they would otherwise silently stop receiving their supplier invoices.

Design rule — alert SLA, resend-until-ack lifecycle, and acknowledgement model. The alert has a defined operational contract, not a single fire-and-forget send:

Invariant: Losing inbound PA-designation stops inbound for that SIREN but does not revoke the issuing mandate and does not stop outbound issuance through Green-Got. Outbound is gated on a valid issuing mandate (scope_flags.issue, status = active) and an active business-account subscription — never on subscription alone, and not on inbound designation. There is no minimum-service / continuity window on our side, and no manual Annuaire de-registration on our part.

6. End-to-End Onboarding Sequence

sequenceDiagram
    autonumber
    actor Customer
    participant App as Green-Got app
    participant PA as plateforme_agreee
    participant B2B as B2Brouter (eDocSync)
    participant PPF as PPF / Annuaire (DGFiP)

    Note over Customer,App: Step (a) — Mandate
    Customer->>App: Open enrollment, review mandate contract
    Customer->>App: Grant mandat (consent)
    App->>PA: Persist mandate consent (person, scope, contract version, timestamp)

    Note over PA,B2B: Step (b) — Account creation (one per SIREN)
    PA->>B2B: GET /accounts?cin_value=SIREN (scheme 0002, idempotency check)
    alt Account already exists for SIREN
        B2B-->>PA: existing account (b2brouter_account_id)
    else No account yet
        PA->>B2B: POST /accounts { country, cin_scheme, cin_value, name, address… }
        B2B-->>PA: 200 account created (b2brouter_account_id)
    end
    PA->>App: Persist b2brouter_account_id at organisation level

    Note over PA,PPF: Step (c) — Enable DGFiP → Annuaire registration
    PA->>B2B: POST /accounts/{account}/tax_report_settings (full dgfip body; PUT .../dgfip to update)
    B2B->>PPF: Auto-register SIREN/SIRET in Annuaire
    PPF-->>B2B: Registered (discoverable; Flux 1 + Flux 10 active)
    B2B-->>PA: 200 dgfip enabled

    Note over Customer,PPF: Step (d) — Routing scope (SIREN-only at MVP)
    Note over App,PPF: Routing scope is SIREN-only at MVP — applied automatically by step (c), no customer choice
    Note over B2B,PPF: SIREN-level Annuaire entry recorded via the dgfip enablement (SIRET / internal-code scope DISABLED, gated behind P-2)

    Note over Customer,PPF: Enrollment complete — customer can issue + receive

Key points:

7. What Green-Got Persists

Enrollment state lives at the organisation level (keyed by OrganisationId, owned by the organisation crate), because there is exactly one enrollment per French legal entity (per SIREN).

Persisted itemLevelNotes
Mandate consent recordOrganisation / enrollmentorganisation_id, scope, accepted_at, consent_version, accepted_by, status (active → revoked/superseded). Green-Got-owned (B2Brouter has no mandate object). Part of the PAF. See §4.1.
b2brouter_account_id (current)Organisation / enrollmentOne per SIREN. The current account id used to act on the customer’s behalf. The organisation/enrollment record holds the live value.
b2brouter_account_id (snapshot)PaTransmissionAn immutable historical snapshot captured per transmission for audit, replay, and migration — records which account carried that transmission. Frozen at transmission time; never updated when the organisation’s current account id changes. See 10. Integration Contracts.
DGFiP enablement stateOrganisation / enrollmentWhether dgfip tax report settings are enabled (Annuaire-registered).
Routing scopeOrganisation / enrollmentSIREN-level only at MVP (0225:SIREN), applied automatically via the dgfip enablement. No SIRET / internal-code choice is captured or persisted — those scopes are DISABLED future states gated behind uncertainty P-2 (§4.4).
Enrollment statusOrganisation / enrollmentPosition in the §5.2 state machine (MandateCaptured → … → Enrolled, plus the KybBlocked / AccountFailed / DgfipEnablementFailed failure states), with retry_attempt / max_retry. Modeled as the first-class Enrollment entity in 12. Data Model.
inbound_designation_lost_atOrganisation / enrollmentTimestamp recorded when the designation sweep detects inbound has moved to another PA (§5.5). Null while Green-Got is still the designated inbound PA. Off-boarding marker only — there is no continuity-window closure date (Green-Got applies unbounded continuity on the audit/archive side).
Designation-loss alertDesignationLossAlertThe resend-until-ack alert record on loss of inbound designation: detected_at, first_alerted_at, resend history, acknowledged_at / acknowledged_by, outcome (§5.5).
scope_flagsMandateEvidencePer-capability flags (issue/receive/e_report/payment_data/archive/support), all true at enrollment from the bundled consent; per-flag revoke/enable via append-only MandateEvidence versions (§4.5).

Design rule: the current b2brouter_account_id is organisation-level — it is never stored on invoicing.invoice or on a bills AP document, and those reference the enrollment to obtain the live account. A PaTransmission is the one intentional exception: it captures the account id as an immutable historical snapshot at transmission time, so the audit/replay/migration trail records which account actually carried each transmission independently of the organisation’s current value. The enrollment holds the current account id; the transmission holds the frozen historical one. These are complementary, not contradictory. See 10. Integration Contracts.

Invariant: Money, identifiers, and KYB facts (legal name, SIREN, SIRET, address) are sourced from the organisation crate’s authoritative record. The B2Brouter account is provisioned from them and kept consistent with them; on a KYB change that affects updatable fields (name, address…), Green-Got updates the account via PUT /accounts/{account}. A CIN (cin_*) legal-identity change (a SIREN/SIRET correction) is not a routine PUT — it is the controlled-reprovisioning edge case in §5.3; and B2Brouter documents the TIN (tin_* FR VAT number) as non-updatable.

8. Related Documents

9. Sources

B2Brouter

0. Documentation Index

B2Brouter API — Documentation Index

The concrete API surface Green-Got integrates with. B2Brouter is the certified Plateforme Agréée (PA) and Peppol access point that performs transmission, format generation, annuaire registration, and e-reporting on the customer’s behalf. The regulatory meaning of these calls is documented in the parent Plateforme Agréée docs.

Documents

Related

1. Overview and Concepts

Overview and Concepts

This document describes B2Brouter’s role as Green-Got’s approved platform and Peppol access point, and the B2Brouter domain model (accounts, contacts, invoices, tax report settings, identifiers) that Green-Got integrates against for French e-invoicing.

1. Terminology

2. What B2Brouter Is

B2Brouter is a certified Plateforme Agréée and a Peppol Access Point. Green-Got sends invoice data to B2Brouter over a REST API, and B2Brouter handles:

Design rule: Green-Got submits structured invoice data; B2Brouter owns format generation, routing, and tax reporting. Green-Got does not generate EN 16931 XML itself in the recommended path.

Green-Got’s legal posture is that of a Solution Compatible: it is the software layer, while B2Brouter is the certified PA that actually exchanges and reports. See 1. Reform Overview for the reform-level positioning.

3. Deployment Zones

B2Brouter exposes distinct zones for development, large-integration testing, and live traffic.

ZonePurpose
SandboxDevelopment and early integration.
StagingLarger integrations and pre-production validation.
ProductionLive traffic.

Authentication and base URLs per zone are covered in 2. Authentication and Environments.

4. Domain Objects

4.1 Accounts

An Account represents a company on B2Brouter.

Invariant: there is one account per SIREN. Multiple SIRETs of the same company resolve to the same account; additional establishments are org units within that account.

ConceptDescription
IdentityKeyed on the company SIREN (extracted from the supplied SIREN or SIRET).
EstablishmentsSIRETs of the same SIREN are org units within the one account.
Tax reportingConfigured per account via Tax Report Settings (see 4.4).
TransportsAn account receives documents through its enabled transports.

Account creation and configuration are detailed in 3. Onboarding Accounts.

4.2 Contacts

A Contact is an invoice counterparty (client or provider) held under an account, carrying the routing information used to send to or receive from that counterparty (identifiers, transport type, document type).

4.3 Invoices

Invoices on B2Brouter are typed. The type distinguishes issued vs received documents and self-billing / simplified variants.

Invoice typeMeaning
IssuedInvoiceAn invoice issued (sent) by the account (outbound, accounts receivable).
ReceivedInvoiceAn invoice received by the account (inbound, accounts payable).
IssuedSelfInvoiceA self-billed invoice issued by the account on behalf of the supplier.
IssuedSimplifiedInvoiceA simplified invoice issued by the account.
ReceivedSelfInvoiceA self-billed invoice received by the account.

4.4 Tax Report Settings

Tax Report Settings configure how an account reports to tax authorities. Each setting is identified by a code. The French code is dgfip; B2Brouter also supports other jurisdictions’ codes (Verifactu, KSeF, TicketBAI, SDI, LHDN). Green-Got uses the dgfip setting; enabling it is described in 3. Onboarding Accounts.

4.5 Identifiers (TIN / CIN / PIN and Schemes)

Accounts and contacts carry identifiers, each paired with a scheme that classifies the identifier’s type/issuer.

IdentifierMeaningExample scheme context
TINTax Identification Number.Tax/VAT schemes (e.g. VAT, TAX).
CINCompany Identification Number.Registration schemes (e.g. SIRET 0009, DIR3).
PINPeppol Identification Number.Peppol participant schemes.

Schemes are drawn from code classifications including ISO 6523 ranges (00010088) and 8XXX ranges.

Design note — FR scheme/field wire values are folded into the mapper. The exact FR wire values — the cin_scheme values for FR SIREN (0002) / SIRET (0009) and the tin_scheme value (9957) for the FR VAT number, the EAS scheme value 0225 (France, used for the Annuaire / Peppol participant identifier), and the full account / contact / invoice field lists exchanged with B2Brouter — are confirmed against B2Brouter staging at implementation and folded into the *_from/to_b2brouter_data mappers; French-specific PPF / Annuaire error codes are folded into the canonical internal error model. These are mapper-layer details that do not affect Green-Got’s canonical internal model, only its B2Brouter serialisation. Resolved by design, not an open question.

0225 is a Peppol EAS / transport scheme, not (confirmed) a cin_scheme. France’s 0225 is the EAS code that identifies the French Peppol/Annuaire participant transport (B2Brouter creates a Peppol 0225 transport when DGFiP is enabled — 3. Onboarding Accounts §4.1). The company-identity schemes on accounts/contacts are 0002 (SIREN) and 0009 (SIRET). Whether 0225 is also accepted as a cin_scheme value (alongside 0002/0009) is not documented and is confirmed/denied on staging (Uncertainties W-1; 8. Staging Verification Matrix). Until then, do not use 0225 as a cin_scheme — treat it strictly as the EAS/transport identifier; the working CIN schemes are 0002/0009.

5. Transport Type Code vs Document Type Code

These two codes are independent and both can apply to a recipient or contact:

Example valueKindMeaning
fr.chorusTransport Type CodeDeliver via Chorus Pro (B2G).
peppolTransport Type CodeDeliver via the Peppol network.
emailTransport Type CodeDeliver by email.
xml.ubl.invoice.chorusDocument Type CodeProduce a UBL invoice in the Chorus Pro profile.

A single recipient is therefore described by a transport (e.g. fr.chorus) plus a document type (e.g. xml.ubl.invoice.chorus).

6. eDocExchange vs eDocSync

B2Brouter offers two integration models. Green-Got uses eDocSync.

ModelAccount managementUse case
eDocExchangeAccounts managed in the B2Brouter web UI; no account create/delete via API. Direct ERP/web + API for a single organisation’s own documents.A company integrating its own B2Brouter account.
eDocSyncWhite-label: accounts are provisioned and managed via API on behalf of end customers.A platform provisioning many customer accounts programmatically.

Design rule: Green-Got provisions one B2Brouter account per customer SIREN programmatically, so it must use eDocSync. eDocExchange cannot create or delete accounts via API and is therefore unsuitable for Green-Got’s multi-tenant onboarding.

7. Related Docs

8. Sources

2. Authentication and Environments

Authentication and Environments

This document describes how Green-Got authenticates to the B2Brouter REST API and which environments and base URLs exist.

1. Terminology

2. Header Authentication

Every request authenticates with HTTP headers — there is no OAuth flow.

HeaderRequiredValue
X-B2B-API-KeyYesThe environment-specific, group-based API key.
X-B2B-API-VersionRecommendedAPI contract date; current default 2026-04-20 (see §3.1).
acceptRecommendedapplication/json for JSON responses.

Design rule: API keys are environment-specific (a staging key is not valid in production, and vice versa) and group-based (scoped to a B2Brouter account group). Green-Got must hold and select the correct key per environment.

3. Environments and Base URLs

B2Brouter exposes three zones. Sandbox and Production share the same host (https://api.b2brouter.net); they are distinguished by the API key — Sandbox keys are prefixed test_ — not by the URL. Staging is a separate host.

EnvironmentBase URLKey formUse case
Sandboxhttps://api.b2brouter.netkey prefixed test_Day-to-day development and early integration. B2Brouter’s recommended starting environment.
Staginghttps://api-staging.b2brouter.netstaging keyLarger integrations and pre-production validation. Shares prod’s rate-limit profile minus throughput (see 7. API Mechanics §2).
Productionhttps://api.b2brouter.netproduction keyLive traffic.

Design rule — which env Green-Got uses for what. Green-Got develops and runs end-to-end integration tests against Sandbox (and Staging for pre-production / load-shaped validation; the staging verification matrix in 8. Staging Verification Matrix is captured there). Production is live customer traffic only. Because Sandbox and Production share a host, the active environment is selected by the key (test_ prefix ⇒ Sandbox), not the base URL — selecting the wrong key on the prod host silently routes to the wrong zone, so key selection is a controlled, environment-scoped operation (see §5).

3.1 API version pinning

The dated X-B2B-API-Version contract is honoured from v2025-10-13 onward. As of this writing the available versions are v2026-04-20 (current default), v2026-03-02, v2025-10-13, and v2025-01-01. Green-Got pins 2026-04-20 and sends it explicitly on every request rather than relying on the group’s configured default, so that a server-side default change never silently shifts the contract. All endpoint shapes, field lists, status enums, and error codes in these B2Brouter docs are pinned to API version 2026-04-20. When B2Brouter publishes a newer dated version, the pin is bumped deliberately after re-validating the affected calls on Sandbox/Staging.

Deployment zones (Sandbox / Staging / Production) are introduced in 1. Overview and Concepts.

4. Worked Example

A staging request listing accounts:

curl 'https://api-staging.b2brouter.net/accounts' \
  -H 'X-B2B-API-Key: <STAGING_API_KEY>' \
  -H 'X-B2B-API-Version: 2026-04-20' \
  -H 'accept: application/json'

The same call against production uses https://api.b2brouter.net and the production key.

5. Key Storage in Green-Got

Green-Got loads B2Brouter credentials and base URLs through its CurrentEnvParameters pattern, so that the staging key resolves in staging and the production key resolves in production, and neither is hard-coded.

Design rule: B2Brouter API keys are secrets — never stored in source, never logged. They are loaded per environment via CurrentEnvParameters.

Note: the exact parameter names and storage mechanism are an implementation concern and are intentionally left unspecified here; this doc fixes only that keys are environment-scoped and loaded via the CurrentEnvParameters pattern.

6. Related Docs

7. Sources

3. Onboarding Accounts

Onboarding Accounts

This document describes the B2Brouter API mechanics for creating and configuring a customer account (the API behind the PA-level onboarding flow): account creation under eDocSync, account update, enabling DGFiP/PPF registration, and testing constraints.

1. Terminology

2. Account Creation (eDocSync)

Green-Got creates a customer account with a single call.

VerbPathPurpose
POST/accountsCreate a B2Brouter account (eDocSync).

Availability: POST /accounts is only available for eDocSync subscriptions (the white-label provisioning model).

Fields are sent inside an account object:

FieldRequiredDescription
countryYesAccount country (ISO 3166-1 alpha-2, e.g. fr).
cin_schemeYesCompany-identity scheme. FR legal entity → 0002 (SIREN); FR establishment → 0009 (SIRET).
cin_valueYesThe SIREN (9 digits, scheme 0002) or SIRET (14 digits, scheme 0009). The SIREN is extracted; one account exists per SIREN.
tin_schemeNoTax (VAT) scheme. For France: 9957 (French VAT number).
tin_valueNoThe French VAT number (FR…, e.g. FR32123456789) — VAT only, never the SIREN/SIRET. Optional at creation: if omitted, B2Brouter derives it from the SIREN when DGFiP tax-report settings are activated.
nameYesCompany name.
addressNoStreet address.
cityNoCity.
postalcodeNoPostal code.
provinceNoProvince / region / department.
emailNoContact email.
phoneNoContact phone number.
rounding_methodNoRounding policy for the account.

Response codes:

CodeMeaning
200Account created.
400Invalid request parameters.
401Unauthorized (missing/invalid/expired API key).
404Not found (e.g. the addressed group/subscription is not resolvable for this key).
409Conflict (the account conflicts with existing resource state — see the SIREN dedupe note in 7. API Mechanics §5.1).
422Unprocessable entity (e.g. blank/duplicate/invalid field — parameter_blank, parameter_taken, parameter_inclusion).

Invariant: one B2Brouter account per SIREN. The SIREN is carried as cin_value (scheme 0002); supplying a SIRET (cin_value scheme 0009) resolves to the SIREN-level account, and additional SIRETs of the same SIREN do not create new accounts. Dedupe on the normalized SIREN (via cin_value, or a Green-Got-owned SIREN uniqueness key) — never on tin_value (which carries the VAT number, not the SIREN). See the dedupe lookup in 7. API Mechanics §5.1.

2.1 Field validation rules

B2Brouter’s published account reference does not enumerate exhaustive per-field length/format constraints, so the rules below are Green-Got’s pre-flight expectations (enforced client-side before the call) plus the provider failure modes observed in the error model (7. API Mechanics §3). Exact provider-side max-length / format / enum constraints are confirmed on staging (8. Staging Verification Matrix row A1) — drive a create with boundary values and capture the 422 shape:

FieldExpected constraint (pre-flight)Provider failure
countryISO 3166-1 alpha-2 (lowercased, e.g. fr)parameter_inclusion if not a known country
cin_schemeenum: 0002 (SIREN) / 0009 (SIRET) for FRparameter_inclusion
cin_value0002 → exactly 9 digits + SIREN Luhn validity; 0009 → exactly 14 digits + SIRET Luhn validityparameter_blank / parameter_inclusion / parameter_taken (duplicate)
tin_schemeenum: 9957 for FR VATparameter_inclusion
tin_valueFR VAT format FR + 2 key digits + 9-digit SIREN (FR\d{11})parameter_inclusion
namenon-empty; provider max length to confirm (A1)parameter_blank
emailRFC-5322-shaped; optionalparameter_inclusion
postalcodeFR 5-digit when country=fr; optional
rounding_methodenum (values to confirm on staging, A1)parameter_inclusion

Design rule: Green-Got validates SIREN/SIRET digit-count + Luhn and the scheme/VAT formats before calling B2Brouter (fail-fast, no wasted round-trip), but treats the provider 422 (parameter_blank / parameter_inclusion / parameter_taken) as the authoritative validation outcome and maps it into the canonical internal error model. Exact provider max-lengths and enum value sets are pinned from the A1 staging capture, not asserted here.

3. Account Update

VerbPathPurpose
PUT/accounts/{account}Update an existing account.

Provider fact (what B2Brouter documents): the B2Brouter PUT /accounts/{account} reference states only that the TIN (tin_value — the FR VAT number) cannot be updated. It does not document cin_value / cin_scheme (SIREN/SIRET) as immutable.

Green-Got handling of a CIN/SIREN/SIRET correction (not a documented provider constraint): treat a CIN change (e.g. a wrong SIREN to correct) as an unconfirmed, staging-gated KYB edge case — likely support-mediated reprovisioning, not a routine PUT field update — until B2Brouter staging proves safe PUT semantics for the CIN. Do not assume “TIN immutable, everything else freely updatable”: the SIREN/SIRET is the routing/legal identity and a correction is handled as an edge case (see 9. Mandate §4.2). The only documented immutable field is the TIN.

3.1 Correction procedure and orphaned-invoice handling

Because the SIREN is the account identity and the Annuaire routing key, a wrong-SIREN correction is not an in-place edit; the documented, runnable procedure is:

  1. Detect and freeze. On discovering a wrong CIN (typically surfaced at KYB review or a routing/Annuaire failure), stop all transmission for the org (the enrollment gate, 14. Implementation Wiring §2) so no further invoices attach to the wrong account. Record the discovery in the ComplianceEventLog.
  2. Reprovision. Create a new account under the correct SIREN (the §2 create flow + §4 DGFiP enablement), wait out the Annuaire propagation (≤ 24h, §4.1), and snapshot the new b2brouter_account_id onto the enrollment.
  3. Confirm safe PUT first (staging-gated). Attempt the lighter-weight PUT /accounts/{account} CIN edit only if a staging round-trip (8. Staging Verification Matrix row A5) proves it is accepted and re-registers the Annuaire correctly; otherwise reprovision per step 2. Until proven, assume CIN is effectively immutable in practice.
  4. Orphaned-invoice handling. Invoices already created/sent under the wrong account are orphans: they are not silently migrated. Any that legally transmitted under the wrong SIREN follow the 7. API Mechanics §10.7.7 tax-deadline / correction path (e.g. an avoir + re-issue under the correct account where a legal document was already delivered); drafts that never transmitted are abandoned with the wrong account. The new account starts its own numbering space (see §3.2). Each orphan resolution is recorded in the ComplianceEventLog with the old and new b2brouter_account_id.

Invariant: a SIREN correction never reuses the wrong account’s transmissions; the legal trail of what was sent under the wrong SIREN is preserved (append-only), and correction is forward-only (avoir + re-issue), never a retroactive rewrite.

3.2 Invoice-number uniqueness across account (re)provisioning

The number uniqueness invariant (4. Sending Invoices §2.2) is per B2Brouter account, not global across B2Brouter. Consequences for reprovisioning:

4. Enabling DGFiP / PPF Registration

French e-invoicing is activated by creating the dgfip tax report setting on the account. The DGFiP tax-report-settings endpoints require API version 2026-03-02 or later (earlier versions do not expose them); Green-Got pins 2026-04-20 (see 2. Authentication and Environments §3.1).

Canonical flow (use the full tax_report_setting body, never a bare {code, enabled} toggle):

  1. Create with POST /accounts/{account}/tax_report_settings, sending the full tax_report_setting body with the version-pinned profile fields (§4.1).
  2. Update an existing setting with PUT /accounts/{account}/tax_report_settings/dgfip — also with the full body (refresh stale fields, re-trigger registration).
VerbPathPurpose
POST/accounts/{account}/tax_report_settingsCreate the DGFiP tax-report setting (enables reporting + PPF registration).
PUT/accounts/{account}/tax_report_settings/dgfipUpdate an existing DGFiP setting (refresh stale fields, re-trigger registration).

4.1 Tax-report-settings body (not just {code, enabled})

Enabling DGFiP is not a bare {code, enabled} toggle. The setting carries the company’s tax profile, which PPF requires for correct Flux 1 / Flux 10 generation:

FieldRequiredDescription
codeYes"dgfip".
start_dateYesDate e-reporting begins.
type_operationYes"services", "goods", or "mixed".
naf_codeYes¹INSEE activity (NAF/APE) code (2-digit, e.g. "62").
enterprise_sizeYes¹"micro", "pme", "eti", or "ge".
reason_vat_exemptNoVAT-exemption reason; defaults to "VATEX-FR-FRANCHISE".
emailNoContact for tax notifications.
auto_generateNoAlways true for DGFiP (non-modifiable): B2Brouter generates the Flux 1 / Flux 10 reports from each submission.
auto_sendNoAuto-transmit the generated reports to the authority; defaults to true.
enabledNoActivate the setting; defaults to true.
annuaire_onlyNoIf true, registers Annuaire / Peppol reception only (no full e-reporting); see the mode note below.

¹ naf_code and enterprise_size are required for the full e-reporting mode. They are not required in annuaire_only (reception-only) mode.

{
  "tax_report_setting": {
    "code": "dgfip",
    "start_date": "2026-09-01",
    "type_operation": "services",
    "naf_code": "62",
    "enterprise_size": "pme",
    "email": "tax@example.fr"
  }
}

Modes — full e-reporting vs annuaire_only. B2Brouter’s create-tax-report-setting supports an annuaire_only mode that registers the company in the Annuaire / Peppol for reception only, without enabling full Flux 1 / Flux 10 tax reports. In that mode naf_code and enterprise_size are not required (the profile is only needed to generate reports). In the default full e-reporting mode (annuaire_only unset / false) the full profile above is required.

Product decision: Green-Got’s MVP uses full e-reporting mode. annuaire_only (reception-only) is documented here for completeness but is not used at MVP.

auto_generate / auto_send — semantics and what disabling means (confirm on staging). These two flags govern report generation and transmission respectively:

Effect of enabling dgfip:

Design rule: Enabling dgfip drives Annuaire registration and the Flux 1 / Flux 10 transports — Green-Got does not issue a separate Annuaire-registration call. But automatic report generation is conditional on the tax-report settings being present and correct: auto_generate/auto_send and the profile fields above govern whether and how reports are produced, so the settings must be created (and kept current) — not merely toggled. This is not the same as “no e-reporting handling exists”: Green-Got still tracks tax-report ids, polls their lifecycle, and downloads the registered report (see §4.2).

4.2 Tax-report ids, statuses, and downloads

Automatic generation does not relieve Green-Got of tracking the reports themselves:

Design rule: registered here is the Flux 10 / Flux 1 tax-report registration (e-reporting), distinct from an AFNOR invoice status — do not conflate them (see 6. Statuses and Webhooks §6).

Design note — FR wire values are folded into the mapper, not the domain. The exact FR wire values used here — the cin_scheme values for FR SIREN (0002) / SIRET (0009), the tin_scheme value (9957) for the FR VAT number, the full account field list on create/update, and the tax_report_settings profile fields — are folded into the *_from/to_b2brouter_data mappers; the French-specific PPF / Annuaire error codes returned by registration are likewise folded into the canonical internal error model. These are mapper-layer details: they do not affect Green-Got’s canonical internal model, only how it serialises to and parses from B2Brouter. The body shapes above are documented from the current B2Brouter reference (API version 2026-04-20); the exact FR enum values (type_operation, enterprise_size, reason_vat_exempt, and the cin/tin scheme codes) are documented by the provider but an exact staging sample is still required — they must be confirmed/pinned on staging (they remain launch-blocking in the Uncertainties register) — capture exact samples per the 8. Staging Verification Matrix.

5. Mandate

The reform requires a mandate — the legal authorization for the PA to act on the company’s behalf (see 1. Reform Overview). B2Brouter’s public API documentation does not describe an explicit “mandate” object; authorization is handled at account level (owner/admin roles in the UI).

Open item: how the mandate is captured and stored in B2Brouter is not documented and must be confirmed. This is recorded as an uncertainty to resolve with B2Brouter support / on staging; do not assume an API representation exists.

6. Testing Constraints

The DGFiP QAS environment rejects real SIREN/SIRET values. Tests must use fictitious Chorus Pro QAS identifiers rather than production company numbers.

Validation rules:

7. Onboarding Sequence

sequenceDiagram
    autonumber
    participant GG as Green-Got (SC)
    participant B2B as B2Brouter (PA)
    participant PPF as PPF Annuaire / DGFiP

    GG->>B2B: POST /accounts (account: country, cin_scheme, cin_value=SIREN/SIRET, name, address, ...)
    B2B-->>GG: 200 account (SIREN-level)
    Note over B2B: SIREN extracted; one account per SIREN

    GG->>B2B: POST /accounts/{account}/tax_report_settings { code: dgfip, start_date, type_operation, naf_code, enterprise_size, ... }
    B2B->>PPF: Register SIREN/SIRET in Annuaire (propagation ≤ 24h)
    B2B->>PPF: Create 0225 transport + enable Flux 1 (B2B) / Flux 10 (e-reporting)
    PPF-->>B2B: Registration acknowledged
    B2B-->>GG: tax report setting created
    Note over GG,PPF: Account discoverable after Annuaire propagation (≤ 24h); reports tracked via GET /tax_reports/{id}

8. Related Docs

9. Sources

4. Sending Invoices

Sending Invoices

This document specifies the outbound B2Brouter API flow Green-Got uses to issue and transmit an invoice: create or look up the recipient contact, create the invoice, send it, and retrieve the generated files.

1. Terminology

2. The Four-Step Outbound Flow

Issuing an invoice through B2Brouter is a four-step sequence:

  1. Create or look up the recipient contact — establish the counterparty and its routing information.
  2. Create the invoicePOST the invoice data (Green-Got submits JSON; B2Brouter generates the structured XML).
  3. Send the invoice — trigger transmission to the recipient via the resolved transport.
  4. Retrieve the generated files — fetch the produced Factur-X/UBL/CII and PDF for archival.

Design rule: Green-Got submits invoices as JSON and lets B2Brouter generate the correct format per recipient. Green-Got never builds the XML itself for the standard path. The XML upload path exists (see 4. Formats and Invoice Data) but is not the default.

2.1 Step 1 — Create or look up the contact

A contact carries the recipient’s identity and routing. It is created once and reused across invoices.

VerbPathPurpose
POST/accounts/{account}/contactsCreate a contact (client and/or provider). Returns 201 with id.
GET/directory/{scheme}/{id}Look up a recipient’s reachable transports/document types by identifier.
GET/directory/{country}/{id}Same lookup keyed by country + identifier.

Fields on POST /accounts/{account}/contacts (in the contact object):

FieldRequiredDescription
nameYesLegal name of the counterparty.
countryYesISO 3166-1 alpha-2 country code.
tin_schemeYesTax identifier scheme (e.g. VAT, TAX).
tin_valueYesTax identifier value.
is_clientNoMarks the contact as an invoice recipient (we issue to them).
is_providerNoMarks the contact as a supplier (they issue to us).
transport_type_codeNoDelivery channel: peppol, fr.chorus, email, etc. Drives routing.
document_type_codeNoOutput document format/profile. Channel-specific: B2G/Chorus Pro → xml.ubl.invoice.chorus; domestic B2B (Flux 1) → the France CIUS profile (UBL/CII frcius, e.g. xml.ubl.invoice.frcius.v1, or Factur-X) — exact B2B string confirmed on staging (D1). Do not use chorus on the B2B path (§1).
cin_schemeNoCompany identifier scheme (e.g. 0009 SIRET, DIR3).
cin_valueNoCompany identifier value.
languageNoCorrespondence language.
termsNoDefault payment terms text.
public_sectorNoFlags a public-administration recipient (B2G).
payment_methodNoDefault payment method metadata.

Directory lookup is the alternative to manually asserting routing on the contact: querying GET /directory/{scheme}/{id} returns the recipient’s supported transport and document types, which can populate transport_type_code / document_type_code. This B2Brouter directory is not the national Annuaire — see 6. Annuaire and Routing.

Directory lookup is NOT a binary found/not-found — it has four outcomes, and a still-syncing participant must never be read as lost routing. The B2Brouter directory endpoint can still be populating its local directory from the Peppol network when queried, so the lookup MUST be modelled as a state machine, not a single success path (verified against the B2Brouter directory-lookup reference — see §9 Sources):

OutcomeHTTPMeaningGreen-Got handling
Found200Company resolved; supported transports / document types returned.Terminal. Use to populate transport_type_code / document_type_code. Cacheable.
Processing(polling_url)202Participant found in the Peppol network but not yet in B2Brouter’s local directory — sync in progress.Non-terminal — do NOT cache. Retry the polling URL with backoff until it resolves to a terminal outcome (200 / 404 / 424).
NotRegistered404The participant is not registered in the Peppol network.Terminal. Recipient genuinely absent (see fail-closed routing rules in §3).
RegisteredButIncomplete(code)424Registered in Peppol, but the participant’s SMP did not provide the data B2Brouter depends on to register it as an exchange partner. codeno_businesscard / incomplete_businesscard / no_exportable_doctypes.Terminal, but not the same as NotRegistered: map 424 to operator remediation (the recipient’s SMP must be fixed), not a silent drop.

Rules:

2.2 Step 2 — Create the invoice

VerbPathPurpose
POST/accounts/{account}/invoicesCreate an issued invoice. Returns 201 with id.

Fields on POST /accounts/{account}/invoices (in the invoice object):

FieldRequiredDescription
typeYesIssuedInvoice, IssuedSelfInvoice, or IssuedSimplifiedInvoice.
contact_idYesThe recipient contact’s id from step 1.
numberYesInvoice number, unique per account.
dateYesIssue date.
currencyYesISO 4217 currency code.
due_dateYesPayment due date.
invoice_lines_attributes[]YesLine items (see below).
type_documentNoUNCL1001 document type: 380 commercial invoice, 381 credit note (avoir), 386 prepayment, 389 self-billed.
buyer_referenceNoBuyer routing/service reference (required for Chorus Pro — see §3).
remittance_informationYes¹Remittance reference carrying the PMD payment data (structured/unstructured remittance reference). Required France B2B native field.
payment_method_textYes¹The PMT payment-method text (how the invoice is to be paid). Required France B2B native field.
payment_termsYes¹The AAB payment terms — due date, penalties, discount conditions. Required France B2B native field.
ibanNoCreditor IBAN (metadata only; B2Brouter does no settlement).
order_numberNoPurchase order reference.
termsNo²Payment terms text. To-confirm legacy/generic alias — possibly a generic/legacy alias of payment_terms; do not rely on it for the France B2B path until confirmed on staging (use the native payment_terms field above).

¹ France B2B native payment fields. remittance_information (PMD), payment_method_text (PMT), and payment_terms (AAB — due date / penalties / discount conditions) are required native fields on the France B2B IssuedInvoice JSON-create path. Omitting any of them returns HTTP 422 (see 7. API Mechanics §3). They are distinct from iban (settlement metadata only).

² terms is kept only as a to-confirm legacy/generic alias pending staging confirmation; it is not the native France field.

Top-level body params (siblings of invoice, NOT inside it). send_after_import and ack are top-level request-body parameters alongside the invoice object — they are not fields of the invoice. The create body is shaped:

{
  "send_after_import": true,
  "ack": false,
  "invoice": {
    "type": "IssuedInvoice",
    "contact_id": 123,
    "number": "F-2026-0001",
    "date": "2026-06-21",
    "currency": "EUR",
    "due_date": "2026-07-21",
    "invoice_lines_attributes": [ /* … */ ]
  }
}
Body paramRequiredDescription
invoiceYesThe invoice object (fields above).
send_after_importNoBoolean. true queues an immediate async send after create.
ackNoBoolean acknowledgement flag.

Each entry of invoice_lines_attributes[]:

FieldRequiredDescription
quantityYesLine quantity.
priceYesUnit price.
descriptionYesLine description.
taxes_attributes[]YesOne or more taxes, each { name, category, percent }.

Each entry of taxes_attributes[]:

FieldDescription
nameTax label (e.g. VAT).
categoryEN 16931 VAT category code (UNCL5305: S, Z, E, AE, K, G, O).
percentRate as a percentage.

Invariant: number is unique per account. Green-Got’s gap-free numbering must be allocated before the create call; a collision returns 422 (taken). See 7. API Mechanics.

2.3 Step 3 — Send the invoice

VerbPathPurpose
POST/invoices/send_invoice/{id}Transmit the invoice via the resolved transport. No request body.

Responses:

If the invoice was created with send_after_import=true, an immediate async send is queued at create time and a separate send_invoice call is unnecessary.

On Green-Got’s side, this whole step runs through a durable transmission queue (Temporal), not a synchronous call — see 7. API Mechanics §10. Transmission Queue and Rate Management.

Correcting a not-yet-sent invoice (draft deletion vs avoir). An invoice created in new (created, not sent — created without send_after_import, 6. Statuses and Webhooks §2.1) has not legally transmitted, so correcting it does not require an avoir. Whether B2Brouter exposes a delete/discard for such a new (draft) invoice — and whether the consumed number is then freed — is not documented and is confirmed on staging (8. Staging Verification Matrix B1):

The dividing line is legal transmission: before send (new) a draft correction is possible (delete-if-supported); after send the correction is always avoir + re-issue, never an edit.

2.4 Step 4 — Retrieve the generated files

VerbPathPurpose
GET/invoices/{id}?include=detailed_linesRetrieve the invoice JSON (header, lines, totals) plus the download references.

B2Brouter distinguishes four file concepts on an invoice, and only one is the legal artefact:

Canonical retrieval for an ISSUED invoice — fetch the legal file:

VerbPath / fieldPurpose
GETdownload_legal_url (from the invoice JSON)The documented way to download the legal file. It is a relative path (e.g. /attachments/download/{id}/{filename}) — prefix it with the environment base URL.
GET/invoices/{id}/as/legalThe same legal document via the document-type endpoint (returns the stored legal document: for issued invoices, the document as delivered).

disposition=inline renders a PDF inline rather than as a download. Green-Got archives the legal file (via download_legal_url) under its retention obligation (see 9. Archiving and Audit Trail). Do not archive an attachments[] entry or a generated export as the legal artefact unless B2Brouter support confirms a specific attachment is the legal file for a specific flow.

3. Routing Behavior

B2Brouter resolves the delivery route at send time:

  1. If the contact carries a transport_type_code, that transport is used.
  2. Otherwise B2Brouter performs a directory lookup to discover a reachable transport.
  3. If no transport resolves, the send fails with 422 (no recipient/route).

Design rule: A 422 on send most often means routing did not resolve — the recipient is not reachable on the asserted transport, or the directory lookup found nothing. This is distinct from a content-validation 422. Inspect the error body and X-B2B-API-Request-Id (see 7. API Mechanics).

No-route during Annuaire propagation — retry, do not hard-fail. A freshly-enabled recipient (or our own freshly-enrolled account) may be not yet discoverable while Annuaire registration propagates (up to 24h3. Onboarding Accounts §4.1). A directory lookup or send that fails only because the recipient is not yet in the directory is therefore treated as transient within a bounded propagation window, not a terminal routing failure:

in_dgfip_annuaire: false is a FAIL-CLOSED legal-non-transmission condition for domestic B2B — not merely a routing rejection. Distinct from the transport-resolution failures above, B2Brouter exposes a contact/invoice flag in_dgfip_annuaire that drives whether a Flux 1 tax report is generated at all (confirmed against the B2Brouter France DGFiP guide — see §10 Sources):

For a domestic FR B2B invoice, in_dgfip_annuaire: false (or any response with no Flux 1 tax report attached) MUST be treated as legally not-sent and fail closed per the post-call validation invariant in §5.1 — it is surfaced, retried/alerted against the statutory deadline, and follows the recipient-not-found / Rejetée correction loop (6. Annuaire and Routing §4.4). It is never silently recorded as a successful send and never down-routed to Flux 10.

4. Chorus Pro (B2G)

Invoices to French public administrations route through Chorus Pro. The contact and invoice carry Chorus-specific fields.

Contact configuration for a public-sector recipient:

FieldValue
transport_type_codefr.chorus
document_type_codexml.ubl.invoice.chorus
cin_scheme0009 (SIRET)
cin_valuethe recipient’s SIRET
public_sectortrue

Invoice requirements for Chorus Pro:

Invariant: Chorus Pro invoices that omit ponumber or buyer_reference are rejected. B2G routing is part of the reform’s invoice scope, not a separate channel — the same outbound flow applies, with the contact/invoice fields above.

5. Automatic PPF and E-Reporting Handling

When the account has DGFiP enabled (see 3. Onboarding Accounts), B2Brouter performs the regulated flows automatically from the same submission:

Design rule — scoped to invoice/transaction data only. There is no separate e-reporting API call for the invoice/transaction data B2Brouter can derive at create/send time: submitting and sending the invoice is sufficient, and B2Brouter selects Flux 1 vs Flux 10 from the transaction’s nature. This does NOT cover the VAT-on-collection payment-data overlay (CGI art. 290 A): payment data is Green-Got-owned (B2Brouter performs no payment processing), is derived from the PaymentCollected ledger, and its exact submission/correction mechanism is a launch-blocking, confirm-on-staging item (Uncertainties L-4, P-7) — it is not “automatic from invoice submission”. Do not read “no separate API call” as applying to payment data. See 2. Platform Architecture and 7. E-Reporting §6.

5.1 Post-call validation invariant — a 2xx is NOT proof of legal transmission

Design rule — a successful HTTP response can be a legally UNSUCCESSFUL invoice; validate fail-closed after every regulated-flow call. A 2xx status and a created/updated invoice object are necessary but not sufficient evidence that an invoice was legally transmitted. The B2Brouter France DGFiP guide confirms two independent ways a “successful” response is legally incomplete (see §10 Sources):

Invariant — fail-closed post-call validation for regulated flows. After every create / import / send / refetch (GET /invoices/{id}) for a regulated flow, Green-Got MUST verify ALL of the following before recording the operation as legally sent — a 2xx alone is rejected:

  1. errors[] empty or non-blocking — any blocking error in errors[] means not transmitted, even on a 2xx.
  2. Expected tax_report_ids present — the response/refetch carries the tax-report id(s) the flow requires (tracked and polled per 3. Onboarding Accounts §4.2).
  3. Expected tax-report type — the attached report is of the expected kind: Flux 1 for a domestic FR B2B invoiced operation, Flux 10 for B2C / cross-border (a Flux 10 report carries a non-null ledger_id). A domestic B2B invoice that yielded only a Flux 10 (or no) report is wrong.
  4. Annuaire / tax-report linkage — for domestic FR B2B, in_dgfip_annuaire is not false and a Flux 1 report is attached and linked.

Domestic FR B2B MUST fail closed. If, for a domestic FR B2B invoice, in_dgfip_annuaire: false OR no Flux 1 report is attached OR a blocking errors[] is present, the transmission is treated as not-legally-sent: it is surfaced (never silently marked success), kept in / re-entered into the durable transmission loop, and tracked against the statutory deadline with pre-breach escalation (7. API Mechanics §10.7.7). It follows the recipient-not-found / Rejetée correction loop (6. Annuaire and Routing §4.4) when the cause is an absent Annuaire entry — never a silent success and never a Flux 10 down-route. This complements the create/import 422 gate (7. API Mechanics §8): a 422 rejects up front, and a 2xx carrying blocking errors[] or missing the expected tax-report linkage is also a failure.

Tax-report / Flux 10 ledger download (supplementary audit evidence). B2Brouter generates Flux 1 / Flux 10 reports automatically and Green-Got tracks them by tax_report_ids and polls GET /tax_reports/{id} (3. Onboarding Accounts §4.2). Whether the generated report / Flux 10 ledger is also downloadable (and in what format) is confirmed on staging (8. Staging Verification Matrix rows C3 / C4):

6. JSON-Create vs XML-Import — Two Distinct Paths

There are two ways to get a document into B2Brouter, and they are different endpoints — not “create then attach”:

Design rule — import, do not create-and-attach. The Green-Got-generated-XML path is POST …/invoices/import, not POST …/invoices followed by add_attachment + send. add_attachment (7. API Mechanics §7) adds supporting attachments to an invoice; it does not supply the primary structured document. Pick exactly one ingestion path per invoice: JSON-create (B2Brouter generates the XML) or XML-import (Green-Got supplies it). See 4. Formats and Invoice Data.

7. Full Send Sequence

sequenceDiagram
    autonumber
    participant GG as Green-Got (SC)
    participant B2B as B2Brouter (PA)
    participant DIR as B2Brouter Directory
    participant PPF as PPF / DGFiP
    participant RPA as Recipient PA / channel

    Note over GG,B2B: Step 1 — contact
    GG->>B2B: POST /accounts/{account}/contacts (name, country, tin, transport_type_code…)
    B2B-->>GG: 201 { id }
    opt Routing unknown
        GG->>DIR: GET /directory/{scheme}/{id}
        alt 200 Found
            DIR-->>GG: supported transports / document types (cacheable)
        else 202 Processing (local-directory sync in progress)
            DIR-->>GG: 202 + polling URL
            Note over GG,DIR: do NOT cache — retry polling URL until terminal
(do not treat as NotFound) GG->>DIR: GET polling URL (retry until 200 / 404 / 424) else 404 NotRegistered DIR-->>GG: 404 not in Peppol network else 424 RegisteredButIncomplete(code) DIR-->>GG: 424 SMP data missing (no_businesscard / incomplete_businesscard / no_exportable_doctypes) Note over GG,DIR: map to operator remediation — not a successful resolution end end Note over GG,B2B: Step 2 — invoice (JSON) GG->>B2B: POST /accounts/{account}/invoices (type, contact_id, number, lines, taxes…) B2B-->>GG: 201 { id } Note over GG,B2B: Step 3 — send GG->>B2B: POST /invoices/send_invoice/{id} alt Route resolved B2B-->>GG: 204 accepted B2B->>B2B: Generate UBL/CII/Factur-X B2B->>PPF: Report data + statuses (Flux 1) B2B->>RPA: Route structured invoice opt B2C / cross-border B2B->>PPF: E-reporting (Flux 10) end else No route / invalid B2B-->>GG: 422 cannot send (X-B2B-API-Request-Id) end Note over GG,B2B: Step 4 — retrieve files GG->>B2B: GET /invoices/{id}?include=detailed_lines B2B-->>GG: invoice JSON (incl. download_legal_url, attachments[]) GG->>B2B: GET download_legal_url (legal file for archival) B2B-->>GG: legal invoice file PPF-->>B2B: CDAR / lifecycle updates B2B-->>GG: status surfaced (webhook / events)

Status surfacing after send is covered in 6. Statuses and Webhooks.

8. Related Documents

9. Sources

5. Receiving Invoices

Receiving Invoices

This document specifies the inbound B2Brouter API flow: how supplier invoices arrive at a Green-Got account, how they are listed and fetched, and how a received invoice becomes an accounts-payable document in the bills/ domain.

1. Terminology

2. Reception Channels

Inbound invoices arrive automatically once the account’s transports are enabled (see 3. Onboarding Accounts). Channels:

ChannelHow it arrives
PeppolDelivered over the Peppol network to B2Brouter’s Access Point for the account.
EmailA document emailed to the account’s reception address.
B2Brouter networkDelivered internally when the sender is also on B2Brouter (P2P).
Manual uploadImported into the account with issued=false.

Invariant: The reception obligation applies to all in-scope French assujettis (Annuaire-registered entities) from 2026-09-01, ahead of the phased issuance obligation. See 1. Reform Overview. An account must be able to receive before it is required to issue.

3. Listing Received Invoices

VerbPathPurpose
GET/accounts/{account}/invoices?type=ReceivedInvoiceList inbound invoices for the account.

Query parameters:

ParamDefaultDescription
typeSet to ReceivedInvoice to scope to inbound.
offset0Pagination offset.
limit25Page size, max 500.
status filtersnew, received, invalid (and downstream accepted, refused, annotated; paid is [open — provider-support] for the French received flow — see §4).
date filtersFilter by date range.
identifier filterstin_value, cin_value, number.

Response shape:

{
  "meta": { "total_count": 0, "offset": 0, "limit": 25 },
  "invoices": [ /* … */ ]
}

See 7. API Mechanics for pagination mechanics.

4. Reception Statuses

StatusMeaning
newManually imported; not yet processed through a transport.
receivedDelivered via a transport (Peppol, email, network).
invalidFailed validation (syntax or business rules).

Downstream statuses are set by Green-Got’s handling of the bill via mark_as — see 6. Statuses and Webhooks. For a French RECEIVED invoice the only confirmed legal mark_as targets are accepted (CDAR 205 Approuvée) and refused (CDAR 210 Refusée). The generic annotated state is also available (internal annotation; does not notify the sender).

[open — provider-support]paid / CDAR 212 (Encaissée, allegedly_paid) is not confirmed for the French received flow. The DGFiP guide ties allegedly_paid / CDAR 212 to the issued lifecycle only; it does not appear in the received-invoice workflow. Treat received-side payment status as carried by Green-Got’s own bills/ (AP) record, not by a B2Brouter received mark_as paid, until provider support for a French received-paid transition is confirmed against staging.

4.1 Received-invoice state-transition matrix

The reception lifecycle has two segments: a provider-set arrival segment (new / received / invalid, set by B2Brouter on ingest) and a Green-Got-driven decision segment (accepted / refused / annotated, set via mark_as6. Statuses and Webhooks §3). The transitions Green-Got relies on for the French received flow:

FromToTriggerFR legal targetNotifies sender?
(none)receivedDelivered via a transport (Peppol / network / email)— (arrival)
(none)newManually imported (issued=false)— (arrival)
received / newinvalidB2Brouter validation failure (syntax/business)
received / newacceptedmark_as accepted (buyer accepts)CDAR 205 Approuvéecommit=with_mail only
received / newrefusedmark_as refused (buyer refuses)CDAR 210 Refuséecommit=with_mail only
received / new / acceptedannotatedmark_as annotated (internal note)— (internal only)No
accepted / refusedpaidmark_as paid[open — provider-support] (CDAR 212 documented issued-only)

Confirm on staging (8. Staging Verification Matrix E4, Q9): that accepted and refused are the only production France received mark_as targets, and whether a bare paid is even honored by the provider for a received French invoice. Paid is not a confirmed received target: received-side payment status is carried in Green-Got’s bills/ (AP) record, not by a B2Brouter received mark_as paid, until provider support confirms a French received-paid transition. accepted / refused are terminal legal decisions; re-deciding an already-decided received invoice is a support-mediated edge case, not a routine transition.

4.2 Polling is the authoritative inbound contract

Design rule — polling is the system of record for inbound state; webhooks are a hint only. Green-Got learns of an inbound document and its state by listing/fetching over the API (GET /accounts/{account}/invoices?type=ReceivedInvoice, then GET /invoices/{id}). A webhook (see 6. Statuses and Webhooks §4) is treated only as a notification that prompts a fetch — it never establishes authoritative state on its own. This keeps the inbound contract correct regardless of webhook delivery, ordering, or payload shape: if a webhook is missed, the next poll reconciles; if a webhook arrives, it only advances the poll. The events feed (6. Statuses and Webhooks §5) is the reconciliation path for the same poll-authoritative model.

5. Fetching Invoice Details

VerbPathPurpose
GET/invoices/{id}?include=linesFetch one received invoice with parsed line data.

Useful parameters:

ParamDescription
include=linesInclude parsed line items (detailed_lines for the fuller form).
disposition=inlineRender the original PDF inline rather than as a download.
ack=trueInclude acknowledged invoices in scope.

Response contents:

Fetching the original document. For a received invoice the original file is the legal artefact (it is the invoice received from the issuer). Download it via the dedicated document-type endpoint:

VerbPathPurpose
GET/invoices/{id}/as/originalReturns the original document as received from the issuer — the artefact Green-Got archives.

(Do not expect a download_legal_url field on received invoices — that field is the issued-side path (4. Sending Invoices §2.4). For received invoices the original is the legal file and /as/original is canonical. /as/legal may also be a valid received legal/original retrieval alias — B2Brouter’s download docs treat document-type download more broadly — so do not reject a received /as/legal retrieval; treat it as an alias to confirm against staging, never as issued-only.)

5.1 Factur-X / ZUGFeRD extraction

When the inbound document is a Factur-X or ZUGFeRD PDF, B2Brouter extracts the embedded CII XML and exposes it as structured JSON in the GET response. Green-Got reads the structured data directly; it does not need to parse the PDF or the embedded XML itself.

Design rule: Always consume the structured JSON for data, and fetch the original document via GET /invoices/{id}/as/original for archival — not an attachments[] entry. The original (XML or hybrid PDF) is the legally retained artifact; the JSON is B2Brouter’s parsed projection of it, and attachments[] are supplemental files, not the invoice.

Format priority on the received side (two artefacts, one rule). A received invoice yields up to two consumable forms, and Green-Got treats them with a fixed priority:

  1. Structured JSON — the data Green-Got reads (header, lines, taxes, totals). B2Brouter normalises every accepted inbound format (UBL, CII, and Factur-X/ZUGFeRD, whose embedded CII it extracts) into the same JSON projection, so the AP domain never branches on the wire format. This is the source for all field reads.
  2. Original document (/as/original) — the legal artefact, archived as-is, whatever its native format (raw UBL/CII XML, or the hybrid Factur-X PDF/A-3).

Green-Got does not prefer one structured format over another for data — it always reads the JSON regardless of whether the original was UBL, CII, or Factur-X. The only format-sensitive choice is on archival, where the original (not a generated conversion) is retained. Confirm on staging (8. Staging Verification Matrix E2/E3): that the structured JSON is populated for each accepted inbound format (UBL, CII, Factur-X) and that /as/original returns the native artefact in each case — capture one received sample per format.

6. Inbound Sequence

sequenceDiagram
    autonumber
    participant SUP as Supplier
    participant SPA as Supplier PA
    participant B2B as B2Brouter (our PA)
    participant GG as Green-Got (SC)
    participant BILLS as bills/ (AP)

    SUP->>SPA: Issue invoice to our SIREN
    SPA->>B2B: Route via Peppol / network / email
    B2B->>B2B: Validate, extract CII (Factur-X/ZUGFeRD) → JSON
    B2B-->>GG: Webhook hint (optional) — prompts a fetch, not authoritative
    GG->>B2B: GET /invoices/{id}?include=lines  (poll = system of record)
    B2B-->>GG: JSON + contact (issuer) + attachments[] (supplemental)
    GG->>B2B: GET /invoices/{id}/as/original  (original = legal artefact)
    B2B-->>GG: original received document
    GG->>BILLS: Emit eventbus event → create AP document
    Note over BILLS: Received invoice becomes a bills/ record (NOT invoicing.invoice)

Polling is the authoritative inbound contract (§4.2); the webhook is only a hint that prompts a fetch. Webhook delivery and the events polling fallback are specified in 6. Statuses and Webhooks.

7. A Received Invoice Is an AP Document, Not an Invoicing Invoice

Invariant: A received supplier invoice is an accounts-payable document. It becomes a record in the bills/ crate. It is NOT an invoicing.invoice and must never be foreign-keyed to the invoicing table.

plateforme_agreee is the transport layer only: it fetches the inbound document and emits an eventbus event. The bills/ crate consumes that event and owns the AP record (matching to a transaction, payment, accounting). Inbound supplier invoices belong to bills (AP); they are never part of the outbound invoicing (accounts-receivable) model.

See the parent integration contracts doc, 10. Integration Contracts, and the bills/ domain documentation for the event payload and AP lifecycle.

8. Related Documents

9. Sources

6. Statuses and Webhooks

Statuses and Webhooks

This document specifies B2Brouter’s invoice status sets, the state-transition endpoints, and the webhook and events mechanics Green-Got relies on to learn of state changes.

1. Terminology

2. Status Sets

These are B2Brouter API statuses. They are a separate layer from the AFNOR XP Z12-012 legal lifecycle and from Green-Got’s internal status; the three-layer mapping (AFNOR ↔ B2Brouter ↔ Green-Got internal) lives in 5. Lifecycle Statuses.

The tables in §2.1–§2.3 are the Green-Got supported subset of B2Brouter’s status enum — the states Green-Got actively observes and maps. B2Brouter’s full provider enum also includes states Green-Got does not currently surface: read, annulled, expired, issued, allegedly_paid, performing_ocr, ocr_failed, billed, payment_processed, and error (on flows where not listed below). These omitted states exist at the provider and may appear on the wire; an unrecognised state must be tolerated (logged, then resolved by refetch/mapping), never assumed absent.

France DGFiP legal states are separate from these generic UI states. The CDAR-backed legal transitions for the French flow (e.g. CDAR 205 Approuvée → accepted, CDAR 210 Refusée → refused, CDAR 212 Encaissée → allegedly_paid/issued-only) are the regulated lifecycle; the generic enum below is B2Brouter’s platform/UI layer. The authoritative AFNOR ↔ CDAR ↔ B2Brouter mapping lives in 5. Lifecycle Statuses.

2.1 Issued invoice statuses

StatusMeaning
newCreated, not yet sent.
sendingTransmission in progress.
sentTransmitted to the recipient channel.
acceptedAccepted by the buyer.
registeredTax-reported (acknowledged by the authority).
refusedRefused by the buyer.
paidMarked paid.
closedTerminal/closed.
errorTransmission or processing error.
downloadedRetrieved by the recipient.

2.2 Received invoice statuses

StatusMeaning
newManually imported.
receivedDelivered via a transport.
invalidFailed validation.
acceptedAccepted (set via mark_as). Confirmed French received target (CDAR 205 Approuvée).
refusedRefused (set via mark_as). Confirmed French received target (CDAR 210 Refusée).
paidMarked paid (set via mark_as). [open — provider-support] for the French received flow: paid / CDAR 212 (Encaissée, allegedly_paid) is documented only on the issued lifecycle. Do not rely on a received mark_as paid for France until confirmed against staging — carry received payment status in bills/ (AP).
annotatedInternal annotation; does not notify the sender.

2.3 Tax-report lifecycle statuses

This is a version-pinned, authority/report-type-dependent enum (API 2026-04-20; DGFiP/French flows). Which states appear depends on the authority and the report type (Flux 1 vs Flux 10), and B2Brouter may introduce new intermediary states. Runtime MUST tolerate unknown future states — log and alert on an unrecognised value, then resolve by refetch/mapping; never panic and never misclassify an unknown state as terminal.

DGFiP tax-report states (the expected France states). These are the states Green-Got expects for the French DGFiP flows. The expected set is report-type-dependent — Flux 1 (domestic B2B) carries annulled, Flux 10 (e-reporting) does not:

StatusMeaningTerminal?Retryable?Flux 1Flux 10
newReport queued (accumulated for the batch).No— (in progress)
sentDeposited to the authority (PPF via SFTP).No— (in progress)
acknowledgedReceived / validated by the authority (PPF).No— (in progress)
registeredAccepted by DGFiP (CDV condition 300).YesNo (success)
refusedRejected by DGFiP (CDV condition 301).YesYes (fix and resubmit)
errorTransmission or processing error.YesYes (transient — retry)
annulledInvoice annulled after registration.YesNo

Generic B2Brouter handling — registered_with_errors. registered_with_errors (registered, with reported errors — terminal, not retryable; review the reported errors) is NOT a DGFiP status. It belongs only to B2Brouter’s generic tax-report handling and must not be treated as an expected France state unless a staging round-trip proves it appears for a French flow. Tolerate it if it arrives (it is terminal-registered), but it is not part of the Flux 1 / Flux 10 expected set above.

Intermediary / authority-specific states exposed by the 2026-04-20 API that may also appear on the wire (authority- and report-type-dependent): processing, signed, sending, deposited, clearing, annullating, invalid. These are transient unless the authority defines them otherwise; treat any not listed in the DGFiP set above as unknown-but-tolerated per the rule above (log + alert, refetch, never panic/misclassify).

VerbPathPurpose
GET/invoice_statesList the available invoice states.

3. State-Transition Endpoints

VerbPathPurpose
POST/invoices/{id}/mark_asSwitch an invoice to a business state.
POST/invoices/{id}/ackAcknowledge an invoice (hides it from default listings unless ack=true).

Body of POST /invoices/{id}/mark_as:

FieldDescription
stateTarget state. Valid values are type-specific — see below.
reasonFree-text reason (e.g. why refused); included in the email when commit=with_mail.
commitwith_mail emails the contact on the transition.

Valid state values by invoice type (provider enum, API 2026-04-20):

Invoice typeAccepted state values
Issuednew, sent, accepted, registered, refused, paid, closed
Simplified issuednew, sent, paid, downloaded, accepted, refused, registered, closed
Self-invoicepaid, accepted, registered, closed
Receivednew, paid, accepted, refused, annotated

The generic mark_as contract supports accepted | refused | paid. For a French RECEIVED invoice, only accepted (CDAR 205 Approuvée) and refused (CDAR 210 Refusée) are confirmed legal targets. paid is accepted by the generic received enum but is [open — provider-support] for the French received flow (CDAR 212 / allegedly_paid is documented issued-only — see §2.2); do not depend on it until confirmed against staging.

commit=with_mail is valid for accepted / refused / paid on received invoices (it notifies the original sender).

Design rule: State changes via mark_as and ack do not consume transaction quota. Only each send and each receive is a billable transaction. Status management is therefore free to perform as often as needed. See 7. API Mechanics.

4. Webhooks

Webhooks are managed via the /web_hooks API (create/list/update/delete — §4.0), not UI-only. On a state change, B2Brouter sends an HTTP POST to the configured Green-Got endpoint. Webhooks subscribe to platform events such as issued-invoice, tax-report, and ledger (Beta) state changes (a separate tax-report webhook fires when the tax-report lifecycle reaches a terminal state), as well as TIN-verification completion.

4.0 Managing webhooks via API

VerbPathPurpose
POST/web_hooksCreate a webhook subscription. Body: a web_hook object (target URL, subscribed events, enabled, description). The response returns the signing_secret — this is the only time it is exposed.
GET/web_hooksList configured webhooks.
GET/web_hooks/{id}Retrieve one webhook.
PATCH / PUT/web_hooks/{id}Update a webhook’s URL, enabled status, subscribed events, or description. It does NOT return or change the signing_secret.
DELETE/web_hooks/{id}Delete a webhook (emergency revocation path — see §4.5).

Signing-secret lifecycle (canonical). B2Brouter returns the signing_secret only when the webhook is created; there is no secret on update and no regeneration endpoint. Therefore:

Because subscriptions are API-managed, webhook configuration is infrastructure-as-code: Green-Got provisions and rotates webhooks programmatically rather than by hand in a UI, and the signing_secret returned on create is stored per the secret controls in §4.5.

4.1 Signature header

Each webhook carries:

X-B2Brouter-Signature: t=<timestamp>,s=<signature>

The signature is an HMAC-SHA256 computed over the string "{timestamp}.{raw_request_body}" using the webhook signing key. Encoding is hex. B2Brouter’s published webhooks-signature guide shows the X-B2Brouter-Signature header carrying a 64-character lowercase hexadecimal s= value — the canonical representation of a 32-byte SHA-256 digest — which is direct evidence the digest is hex-encoded, not base64 (a base64 SHA-256 digest is 44 characters). Design choice: the working constant is hex, and it is still re-confirmed against B2Brouter staging at implementation via 8. Staging Verification Matrix row E3 (capture a real s= value and confirm its length/charset). It is a single mapper/verifier constant; were a future API version to switch encoding, only the encode call in step 4 below changes.

4.2 Verification procedure

Green-Got MUST verify every webhook before acting on it:

  1. Read the X-B2Brouter-Signature header and split it into t=<timestamp> and s=<signature>.
  2. Capture the raw request body bytes exactly as received — do not re-serialize parsed JSON.
  3. Construct the signed payload string signed = timestamp + "." + raw_body.
  4. Compute expected = HMAC_SHA256(signing_key, signed) and apply the same digest encoding B2Brouter uses (assumed hex — see note above).
  5. Compare expected against signature using a constant-time comparison.
  6. Reject the request (do not process) if the comparison fails, and emit an alert on every signature failure (§4.2.1).
  7. Reject if timestamp is outside the acceptable freshness window (replay protection is mandatory — §4.2.1).
  8. Check the processed-event store (signature-id / event-id / raw-body hash); if already seen, ack and drop as a duplicate.

Invariant: Never trust a webhook whose signature does not verify. Use a constant-time comparison to avoid timing leaks. The signed payload uses the raw body — re-serialization will change the bytes and break verification.

4.2.1 Replay protection (MANDATORY)

These controls are required, not optional:

The same identity-based dedupe applies to the events fallback (§5), so a state change recovered by polling is not double-processed against a late webhook.

4.3 Payload schema

The webhook payload schema is published in the B2Brouter /web_hooks API reference (the webhook verification guide still shows only the signature mechanics, but the /web_hooks reference documents the delivered fields). A delivery is expected to carry, at minimum:

FieldUseConfirmed?
event idStable per-delivery identity for dedupe/traceability (feeds the processed-event store, §4.2.1).Confirm on staging (E3 / Uncertainties W-2) — presence of a stable per-delivery event id is not yet proven against a captured payload; the §4.2.1 fallback chain (s(timestamp, raw_body) hash) makes dedupe correct regardless.
invoice id / tax-report idThe resource whose state changed — the key to refetch.Documented
stateThe new provider state (one of the §2 enum values; tolerate unrecognised values per §2).Documented
notesProvider-supplied context for the transition (e.g. refusal reason).Documented

Reconciliation with the W-2 stance. W-2 (replay protection / event-id) stays open precisely because the exact field carrying the per-delivery identity is not yet confirmed against a captured staging payload. This table cites the published schema as evidence the fields above are delivered, while flagging the event id as the one field whose presence/shape E3 still pins. This is not a contradiction: the schema is published, but the launch-relevant fact (is there a usable event id, or do we fall back to s/body-hash?) is what staging closes. W-2 is not launch-blocking (the fallback chain works either way), only confirm-on-staging.

Design choice — treat the payload as a ping; refetch for authoritative state. Even though the schema is documented, Green-Got does not make correctness depend on the payload field layout: a webhook prompts a refetch of the resource (GET /invoices/{id}) and state is read from the API object. This refetch-on-ping design is the resilient default that holds regardless of payload shape, and it is the same poll-authoritative stance the inbound flow uses (5. Receiving Invoices §4.2). The documented fields are used for dedupe and traceability (event id, raw-body hash) and to short-circuit the refetch when the payload already carries what is needed — never as the sole source of truth.

4.4 Idempotency

Assume duplicate POSTs are possible. B2Brouter does not document delivery-once guarantees. Make webhook handling idempotent: derive a stable key (e.g. invoice id + target state, or an event id if present) and ignore a transition already applied. The same idempotency stance applies to the events fallback below.

4.5 Signing-key and secret controls (MANDATORY)

The webhook signing key is a credential. The following controls are required (the request-authentication X-B2B-API-Key has its own lifecycle in §4.6):

4.6 API-key (X-B2B-API-Key) lifecycle (MANDATORY)

The X-B2B-API-Key (the request-authentication credential, 2. Authentication and Environments §2) is a long-lived secret distinct from the per-webhook signing_secret, and it needs its own managed lifecycle — the webhook controls in §4.5 do not cover it:

5. Events Polling Fallback

When a webhook is missed (endpoint downtime, misconfiguration), state changes can be recovered by polling.

VerbPathPurpose
GET/accounts/{account}/events?invoice_id={id}Retrieve state-change events, optionally scoped to one invoice.

Use events to reconcile after an outage. Polling (this events feed plus GET /invoices/{id}) is the authoritative recovery path: webhooks are a delivery hint, and the API object is the source of truth (§4.3). Dedupe recovered events against the same processed-event store used for webhooks (§4.2.1) so a polled change and a late webhook are not double-applied.

6. CDAR Receipts

CDAR (Compte-Rendu d’Acceptation/Rejet) messages are PPF delivery/acceptance receipts for the domestic B2B invoice flow (Flux 1). They flow back through B2Brouter and update the invoice object’s state; Green-Got observes them through the same webhook/events surface. CDAR confirms the delivery and acceptance transitions — sent/accepted/refused (AFNOR Reçue/Acceptée/Refusée). It is distinct from the registered status, which is the Flux 10 tax-report registration (e-reporting), not an AFNOR invoice status — see 5. Lifecycle Statuses §5.1 and 2. Platform Architecture.

7. Status Mapping

The B2Brouter statuses in §2 are an API layer. They must be mapped to:

The authoritative three-layer mapping table lives in 5. Lifecycle Statuses; this document only enumerates the B2Brouter layer.

8. Related Documents

9. Sources

7. API Mechanics

API Mechanics

This document specifies the cross-cutting B2Brouter API behavior a robust integration must respect: concurrency and rate limits, error codes, pagination, idempotency, the query language, attachment limits, and document validation.

1. Terminology

2. Concurrency and Rate Limits

Green-Got is the caller; its job is to throttle and batch its own outbound calls to stay safely inside B2Brouter’s behavior — it is not rate-limiting any third party. Two figures govern this, and they have different provenance:

AspectProductionStaging / SandboxProvenance
Rate limit (per API key)1,000 req/min600 req/minOfficial, published B2Brouter limit. Exceeding it returns 429 Too Many Requests.
Simultaneous requests per key~4 (Green-Got throttle)~4 (Green-Got throttle)Not an official B2Brouter limit. A Green-Got conservative self-throttle, pending B2Brouter support confirmation.

On the rate limit (official). The per-minute ceilings above are documented by B2Brouter. The only documented over-limit behavior is HTTP 429 Too Many Requests. B2Brouter does not currently document Retry-After or X-RateLimit-* headers; whether they are emitted in practice is a staging item to confirm (see 8. Staging Verification Matrix). The worker honors any such header if present and otherwise falls back to its own exponential backoff on 429.

On the concurrency cap (Green-Got throttle, not official). B2Brouter publishes no concurrency limit. The ~4 simultaneous-requests figure is a deliberately conservative Green-Got throttle — a safety margin chosen by Green-Got, not a B2Brouter-imposed ceiling — held pending confirmation from B2Brouter support of any real concurrency guidance. Treat it as a provisional client-side cap; the support-confirmed value (if any) replaces it and is recorded in the staging matrix item (12).

These figures are the budget the transmission worker stays under — not a throughput target. The official per-minute ceiling and the provisional concurrency cap are loaded into the worker’s rate-limit configuration; the worker honors whatever staging actually returns (including any live 429 / Retry-After) rather than hard-coding estimates. See 10. Transmission Queue and Rate Management for how the worker consumes this budget.

Design rule: Stay under the official per-minute rate limit (1,000 prod / 600 staging) with margin, and additionally cap outbound concurrency at the conservative ~4-per-key Green-Got throttle until support confirms otherwise. Schedule batch work (bulk sends, reconciliation polls) with bounded concurrency + backoff. Parallelize for throughput, but the bound is owned by Green-Got’s client.

2.1 Request/response timeout thresholds

B2Brouter does not publish per-endpoint latency SLOs, so Green-Got sets client-side timeouts as a Green-Got default (not a provider figure), tuned by call class:

Call classDefault request timeoutRationale
Standard calls (GET, contact/account create, mark_as, ack, status/list polls)30sOrdinary REST round-trips.
Generation-heavy calls (POST …/invoices, …/invoices/import, send_invoice, /documents/validate)120sB2Brouter generates/validates XML (UBL/CII/Factur-X) inline; allow headroom before treating it as stalled.

These are starting values, confirmed/tuned against observed staging latencies (8. Staging Verification Matrix). A client-side timeout is a transient error (§10.4) — not a terminal failure — because the request may have been received and may even have succeeded server-side; it is retried only after a status refetch (§10.4).

3. Error Handling

Every logged B2Brouter response carries an X-B2B-API-Request-Id header, and error responses additionally carry a request_log_url field in the body. For support correlation, permanently persist only normalized request ids / provider ids / codes / hashes — the X-B2B-API-Request-Id on every call (success and failure), plus normalized error codes and a hash of any raw error body. The request_log_url (a signed bearer URL) and raw error bodies are never kept in permanent storage; they go to a short-TTL, encrypted, access-controlled support vault only if needed and only when non-secret. Never persist signed bearer URLs, secrets, or signatures. See the audit-log rule in §3.2.

HTTPCodesMeaning
400invalid_request_params, invalid_api_version, missing_api_version, api_version_subdomain_mismatchMalformed request parameters, or an unsupported / missing / mismatched API version (see 2. Authentication and Environments).
401unauthorized, missing_api_key, invalid_api_key, expired_api_keyAuthentication failure (including a key used in the wrong environment).
403no_account_groupAuthorized but not permitted (no group / permissions linked to the key).
404resource_missingThe target resource does not exist or is inaccessible.
409conflictCurrent resource state rejects the operation.
422parameter_blank, parameter_empty, parameter_inclusion, parameter_taken, parameter_cannot_changeValidation/business-rule failure (e.g. duplicate numberparameter_taken; immutable field → parameter_cannot_change).
429(no code)Rate limit exceeded (§2) — 429 Too Many Requests, returned without an error-code body.

Code correction. The immutable-field error is parameter_cannot_change (current spelling). An earlier draft used cannot_change; that stale form is not a B2Brouter code and must not be matched on.

Design rule: Distinguish a 422 on send (often a routing failure — no reachable recipient) from a 422 on create/import (a field validation failure). Both carry a request id and a request_log_url; inspect the error body, then persist the request id only (the normalized X-B2B-API-Request-Id + normalized error code + a hash of the raw body, §3.2). The request_log_url is a signed bearer URL: if policy permits, transiently view it or hold it in a short-TTL, access-controlled support vaultnever standard-log it and never commit it to evidence/PR artifacts. See 4. Sending Invoices.

3.1 Retry guidance

3.2 Audit logging of request identifiers

Design rule: For every B2Brouter call, permanently persist only normalized identifiers against the originating Green-Got operation in the audit/event log — the X-B2B-API-Request-Id header, normalized provider ids / error codes, and a hash of any raw error body. These normalized handles are the support-correlation evidence (see 8. Staging Verification Matrix, item 14) and are never discarded, even on success. Privacy: the raw error body and the request_log_url (a signed bearer URL) carry personal data / secrets and are never in permanent storage. If support actually needs the raw body or log url, it is written to a short-TTL, encrypted, access-controlled support vault only when it is non-secret — and signed bearer URLs, secrets, and signatures are never persisted at all (per the field-level retention rules in 13. Privacy and Data Protection: permanent = normalized ids/codes/hashes only; raw evidence = short-TTL encrypted vault; secrets/URLs = never persisted).

4. Pagination

List endpoints use offset/limit pagination.

ParamDefaultDescription
offset0Starting record.
limit25Page size, max 500.

Responses carry a meta block:

{ "meta": { "total_count": 0, "offset": 0, "limit": 25 }, "invoices": [] }

Page by incrementing offset by limit until offset >= total_count. Use the maximum limit (500) for bulk reconciliation to minimize round-trips, within the concurrency cap.

5. Idempotency

B2Brouter exposes no idempotency-key header, so Green-Got owns idempotency on both directions of the boundary and designs it in. The rule is: every outbound operation is keyed on a stable Green-Got id and its outcome persisted, and every inbound event is a ping that is deduped before it has any effect.

5.1 Outbound calls (Green-Got → B2Brouter)

Make create/send operations idempotent from Green-Got’s side:

Design rule: Outbound idempotency is client-owned and key-based: (green_got_id, operation) → persisted outcome. A retry consults the persisted outcome and either returns it or safely re-drives the same operation; it never blindly re-POSTs.

5.2 Inbound webhooks (B2Brouter → Green-Got)

Treat inbound webhooks as at-least-once:

Design rule: Inbound handlers are idempotent and dedup-keyed; a webhook never directly mutates domain state from its payload — it triggers an authoritative refetch, and the persisted event-id set suppresses duplicates.

Invariant: Every outbound call and every inbound event is processed at-most-once in effect: duplicate submissions and duplicate webhook deliveries produce no additional state change. This holds even though B2Brouter offers no idempotency key — the guarantee is Green-Got’s, keyed on stable Green-Got ids and persisted outcomes.

6. Query Language (GET /accounts)

The account listing endpoint accepts an operator-based filter syntax.

Operators:

OperatorMeaning
=Equals.
~Contains / matches.
> < >= <=Comparison.
-Negation.
AND / ORBoolean combination.

Queryable fields: tin_value, tin_scheme, cin_value, cin_scheme, country, name, identifier, created_at, status.

7. Attachments

ConstraintLimit
Max file size (general)≤ 50 MB per file.
Max total size (email transport)≤ 40 MB total.
Allowed typesPDF, XML, office documents, CSV.
BlockedExecutables.

Invariant: Email-transport recipients have a tighter 40 MB total cap than the general 50 MB per file limit; size attachment sets against the recipient’s transport before sending.

8. Invoice Validation

B2Brouter validates documents with XSD (syntax) + Schematron (business rules) — and exposes this both as standalone endpoints and inline at invoice create/import. A malformed invoice on create/import is rejected with 422 and the invoice is not created.

Standalone validation (side-effect-free):

VerbPathValidates
POST/documents/validateSide-effect-free validation of a raw document — accepts application/octet-stream / text/plain. Validates XSD + Schematron for UBL, CII, Factur-X / ZUGFeRD, Peppol BIS, XRechnung, and other EN16931 CIUS formats without persisting anything.
GET/invoices/{id}/validateValidates a provider-stored invoice by its B2Brouter id (XSD + Schematron + B2Brouter rules). Returns 200 with the validation outcome; 404 if the invoice does not exist.

Inline create/import validation (the authoritative gate before send):

VerbPathValidates
POST/accounts/{account_id}/invoicesJSON-create path — the posted invoice object is validated; failures return 422 and no invoice is created.
POST/accounts/{account_id}/invoices/importXML / Factur-X import path (GG-generated UBL/CII or Factur-X PDF) — the imported document is validated on ingest.

On import, validation runs on ingest; on create, the posted object is validated. On either, setting send_after_import: true transmits only if the invoice passes validation — a validation failure surfaces as a 422 (with X-B2B-API-Request-Id + request_log_url, §3) rather than transmitting an invalid document.

Where Green-Got validates. Standalone validation is available for pre-flight (e.g. POST /documents/validate on a freshly generated document, or GET /invoices/{id}/validate to re-check a stored invoice), but the create/import 422 remains the final authoritative gate before send. Concretely:

Design rule — the 422 gate is necessary but not sufficient; a 2xx can still be a legally-failed invoice. Treat the create/import 422 as the canonical, authoritative up-front validation outcome; standalone validation is an early-warning aid, not the gate. But a 2xx/created object is NOT proof of legal transmission: the B2Brouter France DGFiP guide confirms a created invoice (200/201) can carry a non-empty errors[] that blocks PPF transmission (“check it even on successful responses”), and a contact with in_dgfip_annuaire: false yields an invoice with no Flux 1 tax report. So the create/import 422 gate rejects up front and, after every create/import/send/refetch for a regulated flow, Green-Got must run the fail-closed post-call validation invariant — empty/non-blocking errors[], expected tax_report_ids, expected report type (Flux 1 vs Flux 10), and Annuaire/tax-report linkage; domestic FR B2B fails closed on in_dgfip_annuaire: false or a missing Flux 1 report (see 4. Sending Invoices §5.1 and 6. Annuaire and Routing §4.4). Store the validation output — permanently the X-B2B-API-Request-Id, the normalized validation error codes, and a hash of the 422 error body — in the audit/event log (§3.2) against the originating invoice, so a rejected document is fully traceable. Note: the raw 422 error body and any request_log_url may contain personal data and/or signed URLs, so they are never permanently persisted; per the field-level retention rules in 13. Privacy and Data Protection, permanent storage is normalized ids/codes/hashes only, the raw body goes to a short-TTL encrypted vault only if needed and non-secret, and signed URLs/secrets/signatures are never persisted. See 4. Formats and Invoice Data and 4. Sending Invoices.

9. Practical Guidance

10. Transmission Queue and Rate Management

Every outbound transmission to B2Brouter runs through an internal durable queue, driven by a Temporal workflow, so the customer’s “send” is decoupled from B2Brouter’s availability and rate limits and is retried until it reaches a terminal outcome.

Volume context. Real volumes are low — likely fewer than 1,000 invoices per day for the first months, far below B2Brouter’s ~1,000 req/min ceiling (§2). This design is therefore about correctness, resilience, and error/retry handling, not throughput. The rate budget exists so the worker is never the cause of a limit breach, not because we expect to approach it.

10.1 Why a queue

From the customer’s side, the experience is simply “send an invoice.” They neither see nor care how transmission works. On Green-Got’s side, the send is enqueued and handled asynchronously:

Design rule: The boundary between the customer and B2Brouter is the queue. The synchronous “send” only durably enqueues the transmission; the actual B2Brouter call sequence (4. Sending Invoices §2) happens asynchronously inside the workflow.

10.2 The rate budget the worker stays under

The worker treats the §2 limits as a budget it never exceeds, loaded into its configuration:

Design rule: Exact ceilings and the precise rate-limit header names are confirmed against staging at implementation and encoded in the worker’s rate-limit config (§2). The worker is written to consume whatever the config says and to obey live 429/Retry-After responses — it does not hard-code the community-thread estimates, and the figures are not left as an open uncertainty in the design.

10.3 Temporal workflow design

Transmission is a Temporal workflow, matching the rest of the codebase, which already uses Temporal as its durable scheduler (Invoicing 9. Transaction Matching §2, Invoicing 7. Reminders). One durable workflow per transmission owns that transmission from enqueue to settlement.

The workflow stages:

  1. Durable enqueue — the synchronous send starts (or signals) the transmission workflow, keyed on the transmission id. Once started, the transmission is durable: Temporal owns its state across restarts.
  2. Rate-aware dispatch — the send activity runs only within the §10.2 budget. The concurrency cap and per-minute ceiling are enforced at the worker / activity level (e.g. a shared B2Brouter client honoring the cap across all transmission workflows on the key), so independent per-transmission workflows still collectively stay under one budget.
  3. Attempt send via B2Brouter — execute the create + send_invoice sequence (4. Sending Invoices §2) as Temporal activities, capturing X-B2B-API-Request-Id on every failure (§3).
  4. Retry on transient failure (bounded) — rate-limit (429), 5xx, and network errors are transient (§10.4): the activity’s retry policy applies exponential backoff (or the server-supplied Retry-After). Because the workflow is durable, retries continue across restarts — but the loop is bounded, not “retry forever” (§10.3.1). It stops on exactly one of: success, a terminal provider/business error, a deadline-margin escalation (the time-to-deadline margin crosses the warning threshold before the statutory deadline — §10.7.7), or retry-budget exhaustion → dead-letter (§10.7.2).
  5. Stop on terminal/business failure — a terminal error (validation 422, routing-failure 422, buyer/platform rejection — §10.4) is not retried. The workflow surfaces the error into the canonical internal error model (5. Lifecycle Statuses §1.1) and stops; the failure is shown to the user/domain.
  6. Settle on success — on success the workflow records the B2Brouter invoice/transmission id and the resulting lifecycle status, mapping the B2Brouter API status onto the canonical internal status and AFNOR legal status (5. Lifecycle Statuses §5).

10.3.1 Retry stop conditions (the loop is bounded)

The transient-retry loop is never “retry forever”. It terminates on exactly one of these four conditions:

  1. Success — the operation completes; the workflow settles (step 6).
  2. Terminal provider/business error — a deterministic 4xx / buyer-Refusée / platform-Rejetée (§10.4); mapped to the canonical internal error model and surfaced, never retried.
  3. Deadline-margin escalation — while the transmission is still non-terminal, its time-to-deadline margin crosses the warning threshold (e.g. 24h / 4h before the statutory DGFiP deadline); the transmission escalates to SEV1 and pages on-call before the deadline is breached (§10.7.7), rather than silently retrying into the breach.
  4. Retry-budget exhaustion → dead-letter — the transient-retry budget is exhausted without a terminal outcome; the transmission is parked in the dead-letter state for human triage (§10.7.2), never dropped and never looped indefinitely.

These four are the complete stop set; any path that is not one of them keeps retrying with backoff within the rate budget.

Idempotency. The send activity is idempotent and keyed on the stable transmission id (§5): before (re-)issuing a create/send, it consults the persisted outcome for that key, and a retry after a possibly-successful send refetches GET /invoices/{id} before re-sending rather than blindly re-POSTing. A retried or restarted workflow therefore never double-sends.

Status mapping. The workflow’s outcome maps onto the transmission lifecycle exactly as 5. Lifecycle Statuses defines — it does not invent its own statuses:

Workflow outcomeB2Brouter signalLifecycle result (5. Lifecycle Statuses)
Accepted into queue— (internal)Transmission durably pending; not yet a legal status.
Send accepted204 on send_invoiceProgresses toward Déposée (200) and the outbound flow (§5).
Terminal validation/routing failure422Surfaced as a canonical internal error (§1.1); on a platform rejection, Rejetée (213).
Buyer business refusal (later, via webhook)inbound statusRefusée (210) — a terminal business outcome, not a retryable send error.

10.4 Error taxonomy — transient vs terminal

Classification reuses the retry guidance in §3.1 and the canonical internal error model (5. Lifecycle Statuses §1.1) — the worker branches on internal error variants, never on raw B2Brouter strings.

ClassTriggersWorker behavior
TransientNetwork errors, client-side timeout (§2.1), 429 (with Retry-After if present), 5xxRetry with exponential backoff / server-supplied delay, within the rate budget; the loop stops on one of success / terminal error / deadline-margin escalation / retry-budget exhaustion → dead-letter (§10.3.1).
Terminal / business422 validation (bad field), 422 routing (no reachable recipient — 4. Sending Invoices §3), 409 conflict, buyer Refusée, platform RejetéeStop. Map to a canonical internal error variant, surface to the user/domain, do not retry.

Design rule: A retry is only ever appropriate for a transient error. Deterministic 4xx (422/409) repeat the same failure on retry and, for sends, risk duplication (§5) — they are terminal and must be surfaced, not looped.

Design rule — a timeout is transient, but always refetch before retrying. A request/response timeout (§2.1) is classified transient: the server may never have received the request, or may have received and even succeeded on it (the response was just lost). Therefore the worker never blindly re-issues a timed-out mutating call (create / send / mark_as):

  1. On timeout, first refetch the resource stateGET /invoices/{id} (or GET /accounts?cin_value=… for a create, §5.1) — to learn whether the prior attempt actually took effect.
  2. If the refetch shows the operation succeeded, settle on that outcome (no re-send); the idempotency key (§5) records it.
  3. If the refetch shows it did not take effect, retry the operation with backoff.
  4. If the refetch itself times out, do not retry the mutating call — escalate (treat as a transient that has exhausted safe automatic handling: keep within the bounded retry loop, and if it persists, dead-letter / page per §10.3.1 / §10.7.7). A mutating call is never re-driven while the resource’s true state is unknown.

This is the same refetch-before-resend idempotency rule as §5.1 / §10.3.1, made explicit for the timeout case so a lost response never causes a duplicate transmission.

10.5 Enqueue → dispatch → retry → settle

stateDiagram-v2
    [*] --> Enqueued: customer "send" (durable)
    Enqueued --> Dispatching: worker picks up within rate budget
    Dispatching --> Sending: create + send_invoice (idempotent, keyed on transmission id)
    Sending --> Settled: 204 accepted → record B2B id + lifecycle status
    Sending --> Retrying: transient (429 / 5xx / network)
    Retrying --> Dispatching: backoff (or Retry-After), still within budget
    Sending --> Failed: terminal (422 validation/routing, 409, refusal/rejection)
    Settled --> [*]
    Failed --> [*]
    note right of Settled
        Maps to AFNOR / internal
        outbound status (§5 of
        5_lifecycle_statuses.md)
    end note
    note right of Failed
        Surfaced via canonical
        internal error model
        (§1.1 of 5_lifecycle_statuses.md)
    end note

10.6 Invariants and rules

Invariant — no transmission is lost. Every accepted “send” is a durable queue entry owned by a Temporal workflow and retried until it reaches a terminal success (recorded B2Brouter id + lifecycle status) or a terminal business error (surfaced to the user/domain). A restart, deploy, or worker crash never drops an in-flight transmission.

Invariant — the worker never exceeds the B2Brouter rate budget. Across all concurrent transmission workflows on one API key, in-flight requests stay within the ~4-concurrency cap and under the per-minute ceiling, and any 429 / Retry-After from B2Brouter is obeyed. The exact figures live in the worker’s staging-confirmed config, not in scattered constants.

Invariant — send is idempotent. The send is keyed on the stable transmission id with a persisted outcome (§5); a retry or workflow replay consults that outcome (refetching invoice status when a prior send may have succeeded) and never double-sends.

Design rule — only transient errors retry, and the loop is bounded. Transient failures (network, 429, 5xx) retry with backoff, stopping on exactly one of success / terminal error / deadline-margin escalation / retry-budget exhaustion → dead-letter (§10.3.1) — never “retry forever”. Terminal/business failures (422, 409, Refusée, Rejetée) stop immediately and surface through the canonical internal error model. The retry loop is never the place a deterministic 4xx is masked.

10.7 Operational resilience

The durable queue above guarantees no transmission is lost; this section specifies how Green-Got operates that boundary — how it detects degradation, contains failures, recovers, and meets the legal tax deadlines that make e-invoicing failures materially different from ordinary outages. Because volumes are low (§10), the operational risk is not capacity but a stuck transmission silently missing a DGFiP deadline; the controls below are built around that risk.

10.7.1 SLOs and alerts

SLOTargetAlert trigger
Transmission settles (enqueue → terminal success/business-failure)99% within 1h, 99.9% within 24hA transmission stays non-terminal beyond the threshold.
Send-attempt success rate (excluding deterministic business 422/refusals)≥ 99% over rolling 1hTransient-error rate breaches threshold (signals a B2Brouter or routing outage).
Webhook → authoritative refetch latencyp95 < 5 minBacklog of unprocessed events grows.
Received-invoice poll freshnessLast successful poll < 2× poll intervalPolling loop stalled.
429 budget headroom< 50% of per-minute ceiling sustainedApproaching the official rate limit (§2) — unexpected at these volumes, so itself an anomaly.

Alerts page on-call via the standard alerting channel; each alert links to the affected transmission(s) by Green-Got id and the permanently stored X-B2B-API-Request-Id (with the request_log_url, if still needed, retrievable from the short-TTL support vault — §3.2).

10.7.2 Dead-letter handling

A transmission that exhausts its transient-retry budget without reaching a terminal outcome, or that hits an unclassifiable error, is moved to a dead-letter state (it is parked, never dropped — the no-loss invariant holds):

10.7.3 Reconciliation dashboards

A reconciliation view answers “is every invoice that should be at PPF actually there, in the right state?” by comparing Green-Got’s persisted transmission/lifecycle state against B2Brouter’s authoritative state (refetched via GET /invoices/{id} and bulk listing with max limit, §4):

The dashboard reconciles on a schedule and on demand; discrepancies are actionable rows, each keyed to a Green-Got id and stored request ids.

10.7.4 Incident severities

SeverityDefinitionExamples
SEV1Legal/financial exposure imminent or occurringA transmission will miss (or has missed) a DGFiP tax deadline; systemic send failure during a reporting window; data-integrity breach in legal status.
SEV2Degraded but no imminent legal breachB2Brouter outage with retries holding; webhook processing backlog; reconciliation surfaces drift within deadline margin.
SEV3Contained / cosmeticIsolated dead-letter with ample deadline margin; transient alert that self-cleared.

SEV1 always triggers the tax-deadline-breach procedure (§10.7.7) and customer-notification rules (§10.7.8).

10.7.5 RTO / RPO

10.7.6 Replay / backfill

10.7.7 Tax-deadline-breach procedure

E-reporting and e-invoicing carry legal deadlines (DGFiP transmission windows; Flux 1 / Flux 10 reporting cadence). A stuck or dead-lettered transmission is therefore tracked against its deadline, not just its retry budget:

  1. Deadline tracking — each transmission/tax-report carries its applicable legal deadline; the system computes time-to-deadline continuously.
  2. Pre-breach escalation — when remaining margin crosses a warning threshold (e.g. 24h / 4h before deadline) while a transmission is non-terminal or dead-lettered, it escalates to SEV1 and pages on-call before the breach.
  3. Active recovery — on-call re-drives the transmission (fixing the cause for terminal-business failures, or escalating to B2Brouter support with the stored X-B2B-API-Request-Id — and, if still needed, the request_log_url pulled from the short-TTL support vault, §3.2 — for provider-side issues).
  4. Breach handling — if a deadline is missed, the incident is recorded with full evidence (timeline, request ids, root cause), the customer is notified (§10.7.8), and any DGFiP-prescribed remediation/late-submission path is followed.

10.7.8 Customer notification rules

10.8 Operational invariants

Invariant — no transmission silently misses a deadline. Every non-terminal or dead-lettered transmission is tracked against its legal tax deadline and escalates to SEV1 with paging before the deadline is breached (§10.7.7). The no-loss invariant (§10.6) is necessary but not sufficient; durability without deadline-awareness could still breach the law silently, so deadline tracking is a first-class control.

Invariant — recovery is evidenced. Every replay, backfill, and recovery records what it touched and is cross-checked by reconciliation (§10.7.3, §10.7.6), so the system can always demonstrate that a recovery left outbound and received state consistent with B2Brouter’s authoritative state.

Invariant — dead-letter, never drop. A transmission that exhausts transient retries or is unclassifiable is parked in a dead-letter state for triage, never discarded; it remains subject to deadline tracking until terminally resolved.

11. Related Documents

12. Sources

8. Staging Verification Matrix

Staging Verification Matrix

Purpose. This document is the single checklist of B2Brouter request/response samples Green-Got must capture on staging before relying on each integration path in production, plus the open confirm-before-implementation questions each capture answers. Nothing here is a settled fact: every row is a to-capture-on-staging item whose real shape (exact fields, headers, status values, formats) is pinned by an actual staging round-trip and the captured evidence — the durable evidence is the request id (X-B2B-API-Request-Id) plus the extracted schema (field names, types, enum/status strings, header presence), not signed URLs. The request_log_url is a signed bearer URL: persist the request id only; if policy permits, transiently view the URL or hold it in a short-TTL, access-controlled support vault — never standard-log it and never commit it to evidence/PR artifacts (see 7. API Mechanics §3.2). It complements the documented behavior in 7. API Mechanics and the Uncertainties register.

Evidence retention discipline (applies to every capture below). Staging captures are not exempt from the field-level retention rule (13. Privacy §4.1, 7. API Mechanics §3.2) — the same rule applies as in production, with one practical relief: because captures use sandbox/test_ data, they carry no real PII, so a raw fixture from a test_ round-trip is safe to retain as a test fixture. The durable artefact this matrix needs is structure, not values: the field names, types, enum value sets, status strings, error codes, header presence, and the shape of request/response bodies — that is what every “what it answers” column captures, and that is what is committed. Concretely:

  • Durable (commit): normalized request ids / provider ids / error codes / hashes, and the schema (field names, types, enums, status/CDAR strings, header names) extracted from the sample.
  • Short-TTL only: any raw body or request_log_url from a capture is held in a short-TTL, access-controlled location, not committed — even for sandbox data, default to the schema extract, not the raw dump.
  • Never: signed bearer URLs, API keys, webhook signing secrets, and full signatures are never committed to permanent stores, exported staging evidence, or PR artifacts, regardless of environment.

If a capture were ever run against real (non-test_) data, only normalized ids/codes/field names may be retained — the raw body is short-TTL and the secrets/URLs rule is absolute.

1. How to use this matrix

For every row: drive the call against staging (https://api-staging.b2brouter.net, sandbox/test_ keys — see 2. Authentication and Environments), capture the full request and response (headers + body), and attach the schema extract as the evidence answering the “what it answers” column. Persist the request id (X-B2B-API-Request-Id) only; on errors, the request_log_url is a signed bearer URL — if policy permits, transiently view it or hold it in a short-TTL, access-controlled support vault, but never standard-log it and never commit it to evidence/PR artifacts. Where a row maps to a numbered question in §3, resolve that question from the captured sample.

2. Capture matrix

2.1 Account and onboarding

#Path / flowWhat it answersAction
A1POST /accounts (one account per SIREN)Exact create body + response shape; how SIREN/identifiers echo back; 422 parameter_taken on duplicateTo capture on staging
A2GET /accounts?cin_value=<siren> lookup (scheme 0002; SIREN is a cin_* identifier, not tin_*)Pre-create dedupe lookup response; fields proving an account already existsTo capture on staging
A3POST /accounts/{account_id}/tax_report_settings (code: dgfip)The exact DGFiP tax-report settings body that registers the Annuaire + PA designation; which fields are required vs defaultedTo capture on staging (Q2, Q3, Q4)
A4Account/settings response after A3Fields proving Annuaire registration and PA designation took effect (and propagation delay)To capture on staging (Q4)
A5SIRET / internal-routing configuration attemptWhether SIRET-level / internal routing scope is settable via API or only via UI/supportTo capture on staging (Q5)

2.2 Invoice creation, validation, send

#Path / flowWhat it answersAction
B1POST /accounts/{account_id}/invoices (JSON create)JSON-create request/response; inline validation 422 shape (body + request id + request_log_url)To capture on staging
B2POST /accounts/{account_id}/invoices/import (GG-generated XML/Factur-X)Whether /invoices/import is the only legal path for GG-generated XML; accepted content types; validation-on-ingest 422 shapeTo capture on staging (Q6)
B3Create/import with send_after_import: trueValidate-and-send atomically; behavior when validation fails (no transmit)To capture on staging
B4send_invoice (send step)Send response (204?), routing-failure 422 vs validation 422; required vs optional send paramsTo capture on staging
B5Legal/original artifact download (download_legal_url / equivalent)Whether the legal/original artifact is downloadable per flow and in what format (UBL/CII/Factur-X)To capture on staging (Q7)

2.3 Tax reports (Flux 1 / Flux 10)

#Path / flowWhat it answersAction
C1Tax-report generation (auto vs manual)Whether auto_generate / auto_send are required for Flux 1 / Flux 10, and what disabling them impliesTo capture on staging (Q3)
C2GET /tax_reports/{id} (status)Tax-report status lifecycle values actually returned (newsentacknowledgedregistered/refused/error)To capture on staging
C3Tax-report downloadWhether the generated report/ledger is downloadable and in what formatTo capture on staging (Q7, Q11)
C4Flux 10 ledger lifecycle (ledger_id, daily batch) and the concrete ledger-import probe. Capture: the tax_report_ids returned on create/send; the full GET /tax_reports/{id} response (fields: id, status, report type Flux 1 vs Flux 10, ledger_id/batch reference if any, created_at, authority ack/registration timestamps); any download endpoint + Content-Type; the daily-batch grouping (one ledger per calendar day?); retry/refused behavior on a forced refused/error. Probe POST /accounts/{account}/ledgers/import directly ([BETA] New Tax Report API; documented Content-Type: application/octet-stream, body = raw XML payload of the ledger, 201 on success — confirm against the live spec): capture the accepted XML schema, the response fields, the returned tax-report id(s), and the correction/reversal behaviour (how a superseding/corrective ledger is accepted).How Flux 10 ledgers are generated / retried / downloaded / reconciled, and whether /accounts/{account}/ledgers/import is the path for ledger submission; whether a ledger_id / submitted_at / reported_at field exists to compute deadline-breach (ties to 7. E-Reporting §4.3)To capture on staging (Q11)
C5Domestic B2B VAT-on-collection (paiement) data. Capture: the exact endpoint + verb used to submit payment data — explicitly probe both candidate paths: (a) POST /invoices/{id}/mark_as with allegedly_paid/212, and (b) the public POST /accounts/{account}/ledgers/import (raw-XML ledger import, Content-Type: application/octet-stream) — and record which path applies to France Flux 10 payment/acquisition reporting vs the enriched 212/MEN invoiced-payment overlay; the request body fields (collection date, amount, amount-by-VAT-rate / MEN); the response + resulting tax-report id; the correction/reversal path (how a re-report/superseding payment entry or ledger is accepted, and its response). Note the issued mark_as {state:"paid"} transition is documented (observed France state allegedly_paid = CDAR 212); what this probe confirms is the enriched-MEN behaviour — whether the paid transition carries / B2Brouter derives the mention d’encaissement (collection date + amount by VAT rate) as the art. 290 A payment-data report, plus its legal effect and correction semantics (the genuinely-gated part, L-4/P-7).How VAT-on-collection payment data is submitted and corrected (which fields/flow; mark_as vs ledgers/import); whether /accounts/{account}/ledgers/import applies to France Flux 10 payment/acquisition reporting; the launch-blocking L-4/P-7 mechanismTo capture on staging (Q10)

2.4 Contacts and directory

#Path / flowWhat it answersAction
D1Contact create + contact/directory lookupContact create body/response; directory lookup fields used to resolve a recipient before sendTo capture on staging

2.5 Webhooks and received invoices

#Path / flowWhat it answersAction
E1Webhooks create / list / update / deleteThe webhook config CRUD surface and response shapes; signature/secret handlingTo capture on staging
E2Received-invoice polling (GET …/invoices listing)Whether received French invoices are polling-only or also delivered via webhooksTo capture on staging (Q8)
E3A delivered webhook payload (any state change)The actual webhook payload schema + X-B2Brouter-Signature header to verifyTo capture on staging
E4POST /invoices/{id}/mark_as — received French invoicesConfirm the only production France received targets are accepted (CDAR 205) / refused (CDAR 210); separately probe whether the generic bare paid is even accepted by the provider for a received French invoiceTo capture on staging (Q9)

Note on mark_as and CDAR. For a received French invoice the production path is accepted (CDAR 205 Approuvée) / refused (CDAR 210 Refusée) only. The generic B2Brouter mark_as reference exposes a bare paid (not allegedly_paid) as a non-France provider capability; Q9 probes on staging whether bare paid is even honored for a received French invoice, but it is explicitly out of the production France path and stays a provider-support/staging item until confirmed. There is no allegedly_paid received target — no provider evidence supports it. Received-side payment status is carried in Green-Got’s bills/ (AP) record, not by a B2Brouter received mark_as.

2.6 Cross-cutting / audit

#Path / flowWhat it answersAction
F1Any logged responsePresence of X-B2B-API-Request-Id on success and error; presence of request_log_url on errorsTo capture on staging (Q14)
F2Deliberate 429 (rate-limit) probeWhether Retry-After / X-RateLimit-* headers are emitted; the support-confirmed concurrency/rate ceilingTo capture on staging (Q12)
F3API version header behaviorWhich API version to pin for prod (X-B2B-API-Version), and behavior on invalid_api_version / missing_api_versionTo capture on staging (Q1)
F4Offboarding archive/exportThe provider archive/export guarantee on offboarding (what is exportable, in what format, for how long). Its closure home is the recorded contract-verification table in 8. Archiving §5.6.1 (rows 2 Export rights, 3 Exit plan, 7 PA-exit PAF-evidence transfer) — the captured staging evidence feeds those rows, which stay OPEN until verified as PASS.To capture on staging (Q13)

3. Confirm-before-implementation checklist

Each item below must be resolved from a captured staging sample (or, where it is a UI/support/contractual matter, from B2Brouter support) before the corresponding path is implemented for production. The “captured by” column points at the matrix row that answers it.

#Question to confirmCaptured by
1Which API version (X-B2B-API-Version) do we pin for production?F3
2What is the exact DGFiP tax-report settings body (required vs optional fields, values)?A3
3Are auto_generate / auto_send required for Flux 1 / Flux 10, and what does disabling imply?A3, C1
4Which response fields prove Annuaire registration and PA designation?A4
5Is SIRET / internal-routing scope configurable via API or only UI/support?A5
6Is /invoices/import the only legal path for GG-generated XML?B2
7Is the legal/original artifact downloadable per flow, and in what format?B5, C3
8Are received French invoices polling-only or also webhooks?E2
9Confirm received French mark_as is accepted/refused only for production; separately, is the generic bare paid even honored by the provider for a received French invoice (staging/provider-support probe, out of the production France path)?E4
10How is domestic B2B VAT-on-collection payment data submitted and corrected — via mark_as (212/allegedly_paid) or POST /accounts/{account}/ledgers/import?C5
11How are Flux 10 ledgers generated / retried / downloaded / reconciled, and does POST /accounts/{account}/ledgers/import apply to France Flux 10 payment/acquisition reporting?C4
12What is the support-confirmed rate / concurrency limit (replacing the provisional ~4 throttle, §2)?F2
13What is the provider’s archive / export guarantee on offboarding?F4
14Which normalized request ids / error codes / hashes must we persist for audit (per §3.2)? (Confirm presence of X-B2B-API-Request-Id and request_log_url as a schema fact — but request_log_url is a signed bearer URL: persist the request id only, never standard-log or commit the URL.)F1

4. Related Documents

Uncertainties

Plateforme Agréée — Active Register

This is an active, classified register of the open items for Green-Got’s French e-invoicing transport layer. The canonical internal model and cross-crate contracts are settled (10. Integration Contracts, 12. Data Model); this register tracks the legal, provider-support, staging-wire and product-decision items until each is closed.

Status sections. Items move through: §1 Research pending (delegated to Claude to investigate, then close), §2 Resolved decisions (decided here; awaiting port into the named canonical doc), §3 Deferred to implementation (no longer a launch-tracked uncertainty — pinned at implementation against staging).

Convention. “Launch-blocking = yes” means the first regulated transmission for a real customer cannot legally or operationally proceed until the item is closed. A resolved or deferred item that is launch-blocking still carries that obligation — it is flagged below.


1. Research pending (delegated to Claude)

These are the items you asked me to research, document, and then close. They remain open until I return grounded findings; I will document the answer in the named canonical doc and move the item to §3.

L-4 — Payment-data (VAT-on-collection) submission & correction

Question: What is the legally-correct Flux 10 payment-data content, and the correction/reversal path when a reported figure changes after submission? Confirm both as a legal obligation and as a concrete B2Brouter mechanism.

Findings.

P-1 — DGFiP tax_report_settings: exact FR field values

Question: The exact FR enum / profile field values for tax_report_settings that enable Flux 1 + Flux 10 and trigger Annuaire registration. (Body shape is settled; only the FR values are open and must be confirmed on B2Brouter staging.)

Findings.

P-2 — SIRET / internal routing-scope mechanism (post-MVP, gated)

Question: How finer-than-SIREN routing scope (per-SIRET, internal-routing-code, multi-establishment) is declared to B2Brouter / the Annuaire. MVP is SIREN-only and is not gated by this.

Findings.


2. Resolved decisions (awaiting port into canonical docs)

Decided per your answers. Each still needs to be written into the named canonical doc; once ported, it can be deleted from this register. Launch-blocking items remain launch-blocking until ported.

L-1 — Mandate capture: in-app checkbox + TOS clause

Decision. Capture is a single in-app consent checkbox at enrollment, with the binding detail in the TOS. Drafted wording below (legal review still required — see caveat).

L-2 — PA-switch / off-boarding continuity = unbounded

Decision. Green-Got applies no time limit to the losing-PA / off-boarding obligations — which exceeds the statutory floor (PLF 2026 art. 28 / renumbered art. 123: losing-PA minimal-service window 6→12 months, decree-pending — see 3. Actors §5; not CGI art. 289 bis):

Port to 9. Mandate §5.4. Launch-blocking: no.

L-3 — Legal/original downloadable artifact per flow

Decision. Each invoice / bill (and the related transaction view) exposes a plain download button for the original file, with no special restriction for the customer. Outbound issued = the transmitted PA-generated artefact (download_legal_url, stored); inbound received = the stored supplier Factur-X/UBL/CII. → Port to 8. Archiving §5 & 10. Integration Contracts §4. Launch-blocking: yes.

L-5 — Retention & customer export

Decision. Indefinite availability as the product-archive promisegoverned by the canonical four-basis retention model (8. Archiving §2/§5.9, 13. Privacy §4), not an unconstrained “keep everything forever” over PII bytes: the statutory floor (6 yr VAT / 10 yr accounting) is mandatory; beyond it the full PII-bearing artefact is retained under the product-archive-promise basis with customer-exit / DSAR / post-statutory deletion controls; only non-PII integrity evidence (hashes, PAF chain) is truly permanent. An off-boarded customer can request a full export bundle (legal artefacts + data) before any deletion. → Port to 8. Archiving §5/§5.9 (and the §L-2 off-boarding flow). Launch-blocking: yes.

L-6 — B2Brouter DPA / subprocessor closure

Decision (intent). A signed Article 28 DPA with B2Brouter is the required posture and is intended to be in place. STATUS: OPEN — evidence NOT yet recorded. “DPA intended” is not “DPA evidence verified”: the 13. Privacy §7.1 launch gate still shows the signed-DPA reference, owner, signed/review dates, linked artifact, and the coverage matrix (DSAR assistance, breach notification, onward-subprocessor limits, deletion/return, audit rights, data location) as UNFILLED. No real invoice PII may be sent to B2Brouter until those are recorded and the coverage matrix is checked.Close by recording the evidence in 13. Privacy §7.1. Launch-blocking: yes (gate OPEN).

P-3 — Received-French-invoice mark_as paid / CDAR 212

Decision. paid / Encaissée for received French invoices must exist in our internal model first, then be mapped to B2Brouter, AFNOR/CDAR, and any other target. → Port to internal model + 10. Integration Contracts §4 & bills/2. Received Invoice Domain. Launch-blocking: yes.

P-4 — Received webhooks vs polling

Decision (as recorded). Webhooks are used continuously and trusted as authoritative, plus a nightly reconciliation batch to catch anything missed. → Port to 10. Integration Contracts §4. Launch-blocking: no.

⚠️ CONFLICT — needs your decision. This recorded decision (webhooks-authoritative) is the opposite of what the adversarial review recommends and of what every canonical doc now states. The review (and the rest of the doc set: 10 §4, 5 §6.2/§14, bills/1 §4, b2brouter/5 §4.2) treats polling as the system of record and the webhook as an early-poll hint only — because B2Brouter does not guarantee webhook delivery, and the research for L-4 confirmed received-side state must be read by polling/refetch. As applied by this pass, the docs are polling-authoritative with an hourly poll + nightly reconciliation backstop, which still honours your “nightly reconciliation” intent. Either confirm polling-authoritative (recommended — docs already match) or tell me to revert the doc set to webhooks-authoritative. Until you decide, the docs reflect polling-authoritative; this register entry is the open conflict.

P-5 — Submission path for GG-generated XML

Decision. Green-Got-generated XML is submitted to B2Brouter only (single destination). The remaining sub-question — whether /invoices/import is the sole route and who owns EN 16931 conformance — is treated as an implementation detail (see §4, P-5 note). → Port to 4. Formats §7 & b2brouter/4. Sending Invoices. Launch-blocking: yes.


3. Deferred to implementation

Per your “we’ll figure this out implementing” — these leave the active-uncertainty list and are pinned at implementation against B2Brouter staging. ⚠️ marks ones that are still launch-blocking and must be closed before first real traffic even though they are deferred.

#ItemLaunch-blockingPinned at
P-6“Not the designated PA” exact error codeno9. Mandate §5.5
P-7Flux 10 ledger generate / retry / download / reconcileno7. E-Reporting
P-8Support-confirmed rate / concurrency limit (~4 concurrent, ~1000/min prod, ~600/min staging)nob2brouter/7. API Mechanics §10
P-9B2Brouter API version to pin + deprecation signalnob2brouter/7. API Mechanics
W-1⚠️ FR wire values (cin/tin scheme strings, EAS 0225, full field lists, PPF/Annuaire error codes)yes (per flow)b2brouter/3. Onboarding §4
W-2Webhook payload shape & HMAC digest encoding (hex vs base64)nob2brouter/6. Statuses §4
W-3⚠️ Audit-storage exact TTLs (short-TTL support vault + operational logs; auto-expiry verified)yes13. Privacy §4.1
W-4CNIL alignment of operational retention durations (logs, contact/prospect window) — cite the specific CNIL référentiel per duration and confirm against current CNIL guidanceno13. Privacy §4
R-1Daily B2C transaction-count obligation (CGI 242 nonies M) — removal is consistently reported as confirmed for the Sept-2026 package, but re-verify against live Légifrance before relying on its absence; placeholder field carried in the e-reporting modelno7. E-Reporting §3.1

P-5 note (conformance authority): confirm at implementation whether POST /invoices/import is the only route for GG-generated XML and whether Green-Got or B2Brouter is the EN 16931 conformance authority.


4. Related Documents

Cards

Apple Pay

Google Pay

Issuance / order

PIN init

Management

Activation

Blocking / unblocking

Limits

PIN update

Revoking

Mastercard integration

Model

Production

Delivery

Reorder

Clients (applications)

Customer facing

Authentication

General definition

Authentication is the process of verifying that a physical person is who they claim to be. This differs from authorization, which determines what actions a verified person is allowed to perform.

We have two levels of authentication:

Simple Authentication: The basic verification of a user’s identity using a single factor, typically something you know (username/password) or something you can access (an email). This is considered insufficient for sensitive financial operations under current regulations.

Strong Customer Authentication (SCA): As defined by PSD2 (European Directive 2015/2366), requires at least two independent elements from different categories:

Modern authentication systems combine these principles with context-aware security:

The strength of authentication increases with each additional factor verified, which is why sensitive operations require multiple factors to be confirmed simultaneously. This is known as Multi-Factor Authentication (MFA) or Two-Factor Authentication (2FA) when specifically using two factors.

Legal Framework

PSD2 (Payment Services Directive 2): Directive (EU) 2015/2366, the article 97: Requirements for strong customer authentication

RTS about SCA The Commission Delegated Regulation (EU) 2018/389 specifies technical requirements for SCA.

Solution logic

Device Trust Fundamentals

The authentication system is built on a device trust model where users can only perform actions from a trusted devices. These devices are categorized into two trust levels: simple trust for customers with basic profiles, and strong trust for customers who have subscribed to products.

The system maintains device trust through expiration periods, which vary based on the device type and storage capabilities. Permanently trusted device: on device with a secure storage, they can stay trusted forever. Temporary trusted device: On the web, when the use has not checked the box “it’s my computer”, the trusted device has an expiration date.

The devices hold their keys. The database hold the status of those keys.

Sensitive Actions

All sensitive operations require two-factor authentication ; including the trusted device. The sensitive actions include:

Device lock

Security events are classified by threat level - low-level threats trigger a temporary device lock, while high-level threats result in complete device trust revocation.

Authentication factors

The system employs multiple authentication factors working in concert.

  1. Device-Based

    • Trusted device status
    • Device fingerprinting
    • Secure storage verification
    • Last usage timestamp
    • Email OTP
    • SMS OTP
    • Authenticator apps
    • Passkeys
  2. Knowledge-Based

    • Password
    • Secret questions
    • Card serial number
    • ID document numbers
  3. Biometric

    • Facial recognition
    • Voice recognition
    • Behavioral patterns
    • Typing patterns
  4. Location-Based : authentication strength is enhanced through location-based verification, including geolocation checks, phone positioning, and usage time patterns. The system monitors these contextual factors to ensure the legitimacy of user actions and maintain security integrity.

    • Geolocation
    • Phone position
    • Usage time patterns
  5. Enhanced Verification: for higher-risk situations, enhanced verification methods are employed, including KYC services through providers like Ubble, direct customer service interactions via phone or video, and specific physical verification actions such as requesting particular ATM transactions.

    • KYC services (e.g., Ubble)
    • Customer service calls/video
    • Specific physical actions (ATM transactions)
    • Phone number verification

Trust Expiration

Device trust is managed through an expiration system. Devices with secure storage capabilities can maintain permanent trust, while web-based access typically receives temporary trust status. Each device’s trust level is regularly evaluated based on usage patterns and last activity timestamp, with automatic revocation occurring after prescribed periods of inactivity.

Security Monitoring

Continuous security monitoring analyzes behavioral patterns, including usage times, input characteristics, and location patterns. Device management includes detailed tracking of trust levels, comprehensive usage history, and automated expiration management based on activity timestamps.

  1. Behavioral Analysis

    • Usage patterns
    • Time patterns
    • Input characteristics
    • Location patterns
  2. Device Management

    • Trust level tracking
    • Usage history maintenance
    • Expiration management
    • Activity timestamps

Solution models

Authentication model

Each authentication correspond to an authentication event.

Notes

Model

Technical requirements

The following requirements for the SCA are derived from the Commission Delegated Regulation (EU) 2018/389. We must build our solution to satisfy those requirements.

Independence of Elements

This is a particularly tricky one. Since the phone contains almost all the informations that we could ask to a person.

Dynamic Linking

Security Measures

Session Requirements

Exemption possibilities

The SCA regulations allow for exemptions (low value, repeated target…). We choose to no implement those exemptions and to apply SCA 100% of the time.

Authentication Flow

Technical Implementation

Authentication Elements

Knowledge Elements

Possession Elements

Inherence Elements

Authentication Codes

Mobile Business

Mobile retail

Web Business

Web retail

External (APIs)

Internal applications

Authentication

VPN + authenticator ?

Back Office

Legacy

Back office

Mobile retail

Web retail

Codebase

Clients

Writing an HTTP Client

This section is meant to be a guide on how to write an HTTP Client for an external API

All clients live in src/clients/src, so the first step is to create a new folder here with the name of our client with a mod.rs file and add it to lib.rs.

Then we can create a {name}_client.rs file and start defining our Client. Our HTTP clients are based on reqwest so they pretty much all start like this

pub static CLIENT: LazyLock<reqwest::Client> = client();

We define a global request client in since it makes sense to reuse the low level http client to reuse the connections. It does not really make sense to reuse the same between different clients since we can only reuse the connections between the same servers.

Next up we define our Client struct itself and implement it. Our client should be cheap to construct so we only use references here. Alternatively we could also define an inner which stores everything for us and we only reference that. For testing purposes we also implement a new_with_url in addition to new

pub struct GouvClient {
    pub url: &'static str,
}

impl GouvClient {
    pub fn new() -> Self {
        Self {
            url: "https://geo.api.gouv.fr",
        }
    }

    pub fn new_with_url(url: &'static str) -> Self {
        Self { url }
    }
}

We need the new_with_url method to be able to write unit test’s like this.

async fn test_get_city_by_name() {
    let mock_server = MockServer::start().await;

    let body = ResponseTemplate::new(200).set_body_json(get_city_by_name_json());

    Mock::given(method("GET"))
        .and(path(PATH))
        .respond_with(body)
        .mount(&mock_server)
        .await;

    let url = mock_server.uri();

    let client = GouvClient::new_with_url(url.leak());

    let entries = client.search_city_by_name("Paris", 1).await.unwrap();

    assert_matches!(get_city_by_name_fixture(), entries);
}

We can define the actual API operation like this

#[automock]
pub trait GouvApi {
    async fn search_city_by_name(&self, city: &str, limit: u32) -> Result<Vec<Entry>>;
}

const PATH: &str = "/communes";

impl GouvApi for GouvClient {
    async fn search_city_by_name(&self, city: &str, limit: u32) -> Result<Vec<Entry>> {
        let url = format!(
                "{}{PATH}?nom={city}&fields=departement,codesPostaux,code&boost=population&limit={limit}",
                self.url
            );

        let res = CLIENT.get(&url).execute().await?;

        match res.error_for_status_ref() {
            Ok(_) => {
                let body = res.text().await?;

                let deserializer = &mut serde_json::Deserializer::from_str(&body);

                Ok(deserialize(deserializer)?)
            }
            Err(_) => Err(HttpError {
                status: res.status(),
                request_body: None,
                response_headers: res.headers().clone(),
                response_body: Some(res.text().await?),
                url,
            }
            .into()),
        }
    }
}

The advantage of using traits instead of functions is that we could now also define a MockGouvClient that we could use in development or testing. This can be achieve in 2 ways. We can either be done via generics, or impl GouvApi or dynamic dispatch. Using impl or generics is prefered since there is no runtime overhead using impl leads to a cleaner definition but cannot be used for example when storing a value on a struct. Another option than dependency injecting a mock client would be using feature flags to select between both implementations. The feature flags approach results in simpler code because no dependency injection is needed but requires workarounds in other areas to be able to run assertions against it. The feature flag approach might be best suited for dev but not testing but should still be used with care since it spread’s places where production code differs from devlopment code all over the codebase.

Another strong argument for traits is that you can use mockall to automock them which allows you to call functions like this .expect_search_city_by_name().times(1) on GouvClient to run assertions.

Function body

The most basic function body of the operation looks like the code above. We define a static with the PATH so we can later use that for our tests below. Then we make the request and deserialize the response using json. In case we have an unsuccessful status code we constructed our own Error type to log as much information as possible about the reponse. This uses the most basic Error assuming there are no expected errors that you would need to handle. If that is the case we would create an error based on our default error.

Errors

use http_utils::HttpError;
use derive_more::From;

#[derive(Debug, From)]
pub enum Error {
    Http(HttpError),
    Reqwest(reqwest::Error),
    Serde(serde_path_to_error::Error<serde_json::Error>),
}

pub type Result<T> = core::result::Result<T, Error>;


This is the default error implementation for http clients. As you can see it does not use any of the more popular error crates. They are not really useful on your case here. anyhow is build so you can return any error. That is not our Goal here, we try to be as specific about our errors as possible. thiserror also does not make sense here since it allows you easily implement Display for your errors which is the opposite of what we want. We would like to send as much raw information about the error as possible to Grafana so prefer the Debug print of our error over a human written error message with less context. So we derive debug here so we can send that respresentation to Grafana and From to enable the ? operator.

Config

When there is a need for variables that can be changed without deployment the config package is meant to be used. Internally this functionality uses Postgres to store config value. Configs are immutable changing a value of a config created a new version of the config.

Adding a new config value

To add a new config value simply add it to the config variable. The only requirement is that it needs to be serializable.

// Define how the value is represented in the code
// You might need to add input validation via a macro
#[derive(Serialize, Deserialize, Validate)]
pub struct Config {
    id: i32,
    pub feature_flags: FeatureFlags,
// We can add validation to invidual fields to ensure config values don't break the application
    #[garde(range(min = 1, max = 100))]
    pub batch_size: u8
}

Managing the config

The config can be managed though the CLI. Later we will also add a UI to other people can take advantage of the feature as well.

Usage: gg config <COMMAND>

Commands:
  create        Can be used to create a new version of the config
  update        Can be used to update the current version of the config
  revert        Revert to the previous or specified version of the config if there is a valid one
  list          List all configs
  print         Print the config to stdout

CI check

To ensure before deployment that there is at least one usable version of the config available Config is anotated with a proc_macro_attribute that makes a query to postgres to check if there is a valid version available. This ensures the developer cannot forget to create a new version before deploying to production avoiding runtime errors.

Using a config value

You can use a config value like this. Currently if there is an update to the config during the lifetime of the programm it will update the value in 1min at the latest. To ensure this reactive behaviour you always need to access CONFIG directly in locations where you want the value to change between invations. If clone the value or pass a reference for example to feature_flags the value is no longer reactive.

use config::CONFIG;

fn process_analytics_events() {
    if CONFIG.feature_flags.in_memory_analytics_processing {
        //some conditional code
    }
}

Database

This section explain how do we use the database in the project.

Tools

Architecture

Soft Delete Strategy

Tables should implement soft deletes by default using a deleted_at timestamp field unless there is a good reason not to. This approach preserves data for customer support investigations and troubleshooting which proved valuable in the past.

Change Tracking

When more comprehensive change history is needed, we leverage logical replication streams via Supabase ETL to capture all table modifications and store them in Clickhouse. This enables detailed change history for both development and back office operators.

Metadata Field

Each table includes a metadata field to store contextual information about updates, such as:

This data we can then store as part of the Clickhouse row to provide a better change history.

Eventbus

While with EventBridge we used a single Eventbus for everything. I think here its simpler if we use multiple smaller Eventbuses.

The idea is to create the Eventbus in the service.rs file of the service it’s being used for. Then we can add rules to it. Rules are very simplar to EventBridge, they consist of an async callback function that is getting a vector of events and can process them and based on the response of the function either all events are considered handled or just a part of them. The rest will get retried. The second argument is a filter fn. It gets the content of the event and has to sync return a boolean response if the rule should handle the event or not. The third argument controls persistency of the rule invocation for a cerain event. If InMemory is selected we run the processor directly without going though postgres. This is very fast and cheap but has the downside of potential dataloss. Useful for things like page view or screen view events. When Db is selected insert will only succeed if the event is stored in the database. Then processor is invoked as a result of polling the database. This is useful for example as the default for every webhook invocation where we prob want to insert every event into Clickhouse and for some event’s start a Temporal workflow. There is no additional functionality planned except for retry control. Any more complicated code should be a Temporal workflow.

Execution models

There are a few different ways we can execute your code.

Synchronous

If atomicity allows it we idealy just execute all our code before sending a response to a request.

#[handler(domain = "invoice")]
pub async fn create_quote(
    State(state): State<AppState>,
    Input(quote): Input<CreateQuoteWithProducts>,
) -> Result<(), CreateQuoteError> {
    use_cases::create_quote::create_quote(state.pg_pool, quote)
        .await
        .map(|_| Json(()))
        .map_err(CreateQuoteError::from)
}

Synchronous + Asynchronous background background task

If we cannot atomically execute all our code synchronous we can atomically start a background task and run our non-atomic code as an async background task. In the example here its a Temporal workflow but it can also be simpler code like sending an email. This functionality is backed by postgres.

impl Message for StartRecurringInvoiceWorkflow {
    async fn process(self) -> queue::Result {
        RecurringInvoiceWorkflow::start(
            &self.recurring_invoice_id.to_string(),
            RecurringInvoiceArgs {
                recurring_invoice_id: self.recurring_invoice_id,
                schedule: self.schedule,
            },
        )
        .await?;

        Ok(())
    }
}

#[tracing::instrument(skip(pool), err(Debug))]
pub async fn setup_recurring_invoice(
    pool: &db::Pool,
    recurring_invoice: CreateRecurringInvoiceWithProducts,
) -> Result<i32, Error> {
    let mut txn = pool.begin().await?;

    let schedule = recurring_invoice.recurring_invoice.schedule.clone();

    let recurring_invoice_id =
        create_recurring_invoice_with_products(&mut txn, recurring_invoice).await?;

    StartRecurringInvoiceWorkflow {
        recurring_invoice_id,
        schedule,
    }
    .enqueue(&mut *txn)
    .await?;

    txn.commit().await?;

    Ok(recurring_invoice_id)
}

Here we currently need to register our struct later in the messages.rs file like this.

use domain::invoice::use_cases::setup_recurring_invoice::StartRecurringInvoiceWorkflow;
use queue::{Message, Result};
use serde_json::Value;

pub async fn process_message(topic: String, payload: Value) -> Result {
    match topic.as_str() {
        "StartRecurringInvoiceWorkflow" => StartRecurringInvoiceWorkflow::handle(payload).await,
        _ => Err("Unknown topic name".into()),
    }
}

We could also let a macro do this later.

Async

For simple async processing for example of webhook events we used AWS EventBridge before. Now we can use our own Eventbus implementation based on Postgres. If async processes require more than the basic eventbus primitive offers we can use Temporal. Eventbus is mainly meant for tasks where Temporal would be too expensive for.

Health Checking

Defining a health check

Currently health checks are tied to a Service and currently it only makes sense to implement them on those. Health checks can be implemented in the following way.

#[async_trait]
impl HealthCheck for MyService {
    async fn health_check(&self) -> Health {
        Health::Healthy
    }
}

Sadly we have to use #[async_trait] here and cannot rely on the native async traits from Rust since they are not object safe (but more performant) and we need the object safety to store them on the heap.

And thats all that is needed to implement a health check 🥳. When we register the service the health check is registered with it. We call the function every 30 seconds and create a gauge metric from it that can be 0 1 or 2. We can now use this to display availibity metrics, display the current state of the system and to setup alerts and indidents based on this.

Grafana setup

We currently automatically display health checks in Grafana.

Futher improvements

This is the first step and we will see what else we need. I could see us give the health check function access to more tools it might need to do health checks. Also we might want to allow health checks independent of a Service.

Parameters

To define variables that do not need to change between deployments and are environment specific the parameters feature is meant to be used.

To define a new parameter that you want to use in the codebase you add them to parameters.rs. There are 2 buildin types available (Secret, Text) but you can use any non heap allocated datastructure you like.

pub struct Parameters {
    pub efficiale_client_id: Text,
    pub efficiale_client_secret: Secret,
    pub some_other_secret: Secret
}

Then each environment has a separate file where the values are defined. Normal parameter values are passed in like they are defined in the Parameters struct. Secrets can be constructed from plaintext for development values. For production they need to be passed in an encrypted form.

pub fn development() -> Parameters {
    Parameters {
            pub efficiale_client_id: "the client id",
            pub efficiale_client_secret: Secret::from_plaintext("plain text secret"),
            pub some_other_secret: Secret::from("osadhasuidhad9ahdg9"),
    }
}

You can use the parameters in the codebase in the following way.

use env::PARAMETERS;

pub fn fetch_efficale() -> Parameters {
    println(PARAMETERS.efficiale_client_id);

    //Secrets are different than normal data you need to call expose to get to the decrypted value
    println(PARAMETERS.efficiale_client_secret.expose());
}

CLI

To help you manage secrets the cli has the following commands available

Usage: gg env <COMMAND>

Commands:
  encrypt
  decrypt
  get

Encrypt is available to everyone, decrypt for production only to a limited group of people. The get command is intended to be used by pulumi to have a bridge between the rust and the typescript code.

Compile-time secret validation

By using the secret macro you can validate your secrets at compile time.

#[secret]
pub fn development() -> Parameters {
    Parameters {
        aws_secret_access_key: Secret::from("bxWJJ8auRMkg3xriNTYjZbz4")
    }
}

For each failing secret you will get the related key highlighted as an error.

Depending on the file, the macro will check locally or remotely. For the production.rs file, an HTTP request will be sent to https://green-got.co/env/verify_secrets. This endpoint uses the same local function as for staging.rs and development.rs to verify the secrets.

This solution avoid sharing the production encyption/decryption key in development-like environments. Note that sqlx has a similar feature for verifying queries at compile time. They describe it in this issue with its pros and cons.

Scripts

Creating a script

To create a script we need to add a file to the src/scripts/src folder or any subdirectory. The smallest script looks like this

#[macros::script]
fn main() {}

Running a script

Scripts can be run using gg scripts <module_path> and should autocomplete. By default scripts run against the local dev environment but can use the --env flag to change that.

Alternatively you can use the little Run Test button above the script to achieve the same thing. Why it says Run test I explained in the Internals section.

Running script in CI Scripts use the familar CI setup we already use where we can preview run the script on the PR and again when merging into main.

Internals To run a script without reinventing cargo we can consider 4 options

  1. cargo script

The issue here is cargo script only works when defining per file dependencies – not what we want

  1. cargo run

Pretty straight forward, you have a /bin folder and you can run and get IDE support for every file in this folder. The only issue is you cannot nest files or you loose both benefits. You can get IDE support back by referencing the file in a non nested file. You can get support for running nested scripts by either moving the file before running it or editing main.rs

  1. cargo test / cargo bench

They are pretty much the same thing for us. Compared to cargo run they have the advantage to be able to be executed from nested folders without moving. The VS Code Codelens action is always usable (although it gives you test output). As you might have seen above I am in favor of the cargo test based approach. The only thing we need to do is surpress the default test output and we are basically there.

Tests

Running tests

Tests can be run by calling gg test. You can run only a select number of tests by running gg test <module_path>. So running gg test utils runs all tests in the utils crate while gg test utils::id runs all tests contained in the id module.

Writing tests

In rust usually the tests are in the same file as the code just separated into a separate tests module. A very basic test would look like this.

// This macro tells rust the module is test only
#[cfg(test)]
mod tests {
    #[test]
    fn test() {
        assert_eq!(1 + 1, 2);
    }
}

To make this test work in our setup we basically just need to use our own test macro.

#[cfg(test)]
mod tests {
    #[testing::test]
    fn test() {
        assert_eq!(1 + 1, 2);
    }
}

This does the following

  1. Enabled logging by outputting our traces to the stdout
  2. Enable us to write async tests
  3. Enabled us to write tests that interact with the database
  4. Installs default mocks for shared external dependencies such as KMS and Payment Cryptography.

Use explicit opt-outs when a KMS smoke test needs the real service:

#[testing::test(kms_mock::disable)]
async fn kms_smoke() {
    // ...
}

For Payment Cryptography, tests always get the default mock. Smoke tests that must hit AWS should live in ignored integration tests that do not use #[testing::test].

#[cfg(test)]
mod tests {
    #[testing::test(Db)]
    async fn test() {
        let organisation = get_organisation(&*DB_POOL, 1).await;

        assert_eq!(organisation, None);
    }
}

Adding Db to the macro initalizes our DB_POOL global to a database connection just for this test with applied migrations. We could have also by detecting that test(db_pool: db::Pool) is looking for a db_pool instead of Db annotion but I believe PG_POOL is worth living in a global.

This is just a starting point since we need to test a lot we want to make sure testing is as easy as writing the code.

Commercial Domain

0. Documentation Index

Commercial Domain — Documentation Index

The commercial_domain/ umbrella holds the crates that run Green-Got’s commercial model: how customers are onboarded, what they can buy, what they’re subscribed to, how they’re billed, and how that revenue is accounted. It mirrors the business_domain/ pattern — a flat grouping of workspace-member crates with no parent crate.

The spine

Crates

CrateStatusWhat it ownsDocs
offersimplemented (v1 slice)Offer / Module catalogue / Step dictionary + composition, eligibility, visibility, lifecycleoffers/docs
onboardingimplemented (v1 slice)the consolidated cross-segment onboarding orchestrator (data-driven Step engine, validation, provisioning)onboarding/docs
subscriptionsdesign / scaffoldingholder↔offer relation, offer changes, bundles, snapshot+filiation, lifecyclesubscriptions/docs
customer_billingdesign / scaffoldingGreen-Got’s own customer billing subledger (BillableEvent/Invoice/CreditNote/Payment/Refund/Statement)customer_billing/docs
accountingdesign / scaffoldingrevenue recognition, partner commission/clawback, VAT, reconciliation (GL view)accounting/docs

Domain boundaries (owns vs references)

commercial_domain records and drives commercial / subscription / billing / onboarding state. It references — but does not own — banking accounts/IBANs (core_banking), identity/KYC (physical_person), organisations (organisation), and savings/insurance contracts (investment). Onboarding is the one crate that depends on those provisioning targets (it is the cross-domain seam); the catalog crates stay segment-neutral data + logic.

Distinct from look-alikes elsewhere in the repo: the invoicing crate is AR for a business customer issuing to their clients; accounting_export exports a B2B customer’s own books. customer_billing + accounting here are Green-Got billing and accounting its own revenue.

0. Use-Case Catalogue

Commercial Domain — Use-Case Catalogue

This is the cross-crate spine of the commercial domain (onboarding, offers, subscriptions, customer_billing, accounting). It enumerates, first-principles, the use cases the model must serve. Every business rule in the per-crate docs and every test in the per-domain test plans traces back to a use-case ID here (rules ↔ use-cases loop): a rule with no use case is unjustified; a use case with no rule is uncovered. The spine separates commercial policy from non-waivable payment-service / compliance rights: commercial rules may choose a product posture, but they never override the regulatory perimeter listed in §9.

Status. This is a working catalogue under active challenge. Open points are marked ⚠️ OPEN. Billing documents deliberately stop at billing/accounting-source data; GL account mapping and double-entry journals live in a separate accounting integration layer.

How to read this

Notation.

ID scheme. UC-<AREA>-<NN>. Areas: J end-to-end journey · ONB onboarding · OFF offers/catalog · SUB subscriptions · BIL billing · ACC accounting · MIG migration/transition · BO back-office & dead-ends · REG non-waivable payment-service/compliance rights. IDs are stable — never renumber; retire with a tombstone instead.

Locked premises the catalogue assumes — the full record is Appendix A. In brief: catalog crates are segment-neutral while onboarding is one consolidated orchestrator ([UC-ONB-13]); an offer change is always close-then-open; upgrades immediate when collectable ([UC-SUB-06]), downgrades keep perks to period end ([UC-SUB-07]), no voluntary unused-time refund (legal refunds/corrections still exist, [UC-BIL-09]); commercial eligibility is continuous ([UC-OFF-04]); price is snapshotted; savings are intermediated, free, commission-funded.


Core entities & domain boundaries

Core entities (each defined once; see the cited UC):

EntityWhat it is
Offerpriced commercial SKU; composes Modules ([UC-OFF-14]) + declares required Steps ([UC-ONB-02]); immutable versions ([UC-OFF-06])
Moduleatomic functional unit from the Module catalogue ([UC-OFF-14]) — payment account, shared account, card (sub-module), livret, ASV, PER
Stepa reusable onboarding requirement from the Step dictionary ([UC-ONB-02])
Subscriptionliving holder↔offer relation; snapshots offer/module versions + price ([UC-SUB-32]); has dates, cadence, status, customer modifiers ([UC-SUB-29])
Accountthe banking object (IBAN, balance, ring-fence); tier-agnostic; ≥1 per bundle subscription ([UC-SUB-12])
Cardsub-module of an account ([UC-SUB-16])
Holder / Participantthe holder owns the account + money; a participant is an authorized user ([UC-ONB-12]/[UC-SUB-26])
BillableEvent / Invoice / CreditNote / Refund / Statementbilling-subledger objects (§5)
DataBagtransient per-onboarding working state ([UC-ONB-26])
Onboarding ownerthe identity a session belongs to — an anonymous prospect token or an authenticated user ([UC-ONB-28])

Domain boundaries — what commercial_domain owns vs references:

OwnsReferences (owned elsewhere)
Offers / Modules / Steps catalogue; eligibility; versioningthe banking account / IBAN / ledger (core_banking)
Subscriptions + lifecycle; offer changes; bundlesidentity / KYC verification (physical_person); AML decisions (compliance)
customer_billing subledger (invoices, credit notes, refunds, statements)savings / ASV / PER contracts (investment + partners)
accounting (revenue recognition, partner commission, VAT)payment execution & disputes / chargebacks (payments domain)
onboarding orchestration (the cross-domain seam, [UC-ONB-13])B2B AR invoicing (invoicing); e-invoicing transmission (plateforme_agreee)

This domain records and drives commercial / subscription / billing state; it references banking, identity, investment, and payment objects but does not own them.


1. End-to-end journeys (the “complete” cases)

These thread a single customer through onboarding → subscription → provisioning → billing → accounting, so a reviewer can see a whole story. The atomic cases in §2–§8 are referenced.

UC-J-01 — Brand-new customer, from nothing, to an active paid offer

UC-J-02 — Brand-new customer, from nothing, to an active free offer

UC-J-03 — Existing customer subscribes to a second offer (onboarding from an existing account)

UC-J-04 — Existing customer upgrades (product migration), mid-period

UC-J-05 — Legacy customer migrated from agent-era plan to a new independent-PI offer


2. Onboarding (onboarding)

UC-ONB-01 — Subscribable-offers gate, from nothing

Prospect requests the list of offers they can subscribe to. The offer segment (retail vs business — the offer’s segment) is fixed by the API surface, not a request field: retail_api lists Retail, business_api lists Business, so a client can’t list the other segment’s offers through the wrong surface. The country of residence is the eligibility input (offers differ by country); it is required on business and optional on retail, which defaults to FR — every retail offer is FR-resident-only and the retail app has no country screen. On the business segment a legal form also applies (e.g. auto-entrepreneur, EURL, SAS); on retail no legal form applies (retail offers leave available_legal_forms empty). The route returns only eligible, non-colliding offers for that country (+ legal form on business)

Offer-side availability declarations. Each offer declares an available_countries_of_residence list and an available_legal_forms list (3. Offers data model); the gate keeps an offer only when the prospect’s country is in the former and — when a legal form is supplied — it is in the latter (an empty available_legal_forms means the offer is unrestricted by legal form, e.g. retail offers). Today there is a single business offer per (country, legal form), so filtering changes nothing yet; it is the seam future multi-offer catalogues hang on. Only publicly accessible offers are returned unless the prospect’s email/phone is whitelisted for restricted offers ([UC-OFF-12]). From nothing, all publicly accessible base offers eligible for that country+segment are returned. The gate is unauthenticated — reachable from the logged-out “open an account” surface; a purely anonymous prospect ([UC-ONB-28]) supplies the country of residence where the surface collects one (business), or it defaults (retail → FR). → [UC-ONB-18] generalises this for existing (authenticated) customers.

Campaign codes & deeplinks (deliberately loose). Beyond the default gate, a prospect can arrive with a code — carried in a deeplink (/…?code=XYZ) or typed into the app — that launches a specific onboarding and/or surfaces specific offers for a campaign. This is intentionally a generic, loose mechanism: a code maps to a campaign config (which offers to show, which onboarding to start, any attached promo / prime / whitelist entry), defined by marketing/growth in the back office without bespoke engineering — so the technical team can say “yes” to a new campaign by configuring a code, not building a feature. The event-card QR ([UC-ONB-19]) and the restricted-offer whitelist ([UC-OFF-12]) are specific instances of this; promo/referral codes ([UC-BIL-19]/[UC-BIL-20]) may ride the same entry. Code lifecycle and config (validity window, max usage count, per-person use) are specified in [UC-ONB-24].

UC-ONB-02 — Offer-driven onboarding creation

Shared Step dictionary + per-offer selection (foundational). There is a single dictionary (registry) of reusable onboarding Steps — e.g. get_email (verify email → mint authenticated session; first on the unauthenticated path, [UC-ONB-31]), get_phone (verify phone; second, [UC-ONB-31]), personal_info, id_doc/PVID, proof_of_address, funds_origin, topup, parental_consent, siret_lookup, kyb_docs, recap — each defined once and reused across offers. The dictionary lives only in code (the Rust STEP_REGISTRY, with each Step’s code, name and kind as typed trait methods) — it is not a database table a non-engineer edits; a boot assertion fails fast if any offer requires a Step the registry does not define. Every commercial offer declares the subset of Steps it requires (its required_steps, referencing dictionary Steps by code); it never invents inline steps. The same Step (e.g. id_doc) is the same registry entry wherever it appears. B2B and retail Steps share one dictionary, each gated by applies_to(offer, bag) ([UC-ONB-13]); adding a capability for marketing ([UC-ONB-24]) is configuring an offer’s Step subset, not building a flow.

The front end requests creation for a chosen offer, but only the backend writes — the FE never persists onboarding state itself. The backend creates the onboarding session (with a DataBag) from the offer’s declared required Steps (its selection from the dictionary). Onboarding is always tied to one offer (a bundle is a single offer → one onboarding).

Per-step submit protocol. Each completed Step is submitted to the backend, which validates it and responds either invalid — a clear, field-level error the FE shows so the user can fix it (the Step is not advanced) — or valid — content recorded in the DataBag, and the next Step returned by the resolver ([UC-ONB-03]). (Step ordering and resume are owned by [UC-ONB-03]; here we cover only the create + submit/validate contract.)

UC-ONB-03 — Resolve-next-missing-Step + deep link (front/back split)

The backend owns the truth. The front end identifies itself to the backend — by an authenticated session or, for a logged-out prospect, the anonymous onboarding/prospect token ([UC-ONB-28]) — and asks for state; the backend runs the resolver (required Steps − satisfied DataBag = next Step) and tells the FE “this owner has an ongoing onboarding at step X”. The FE never decides the next step — it only renders the module for the step the backend names.

The FE chooses presentation, not logic. Given “ongoing onboarding at step X”, the FE decides how to surface it:

Landing on /{onboardingId} redirects to /{onboardingId}?step=<next-missing>; the resolver re-runs after each submit. If the offer is retired or its onboarding Step set changes while an onboarding is in flight, the backend archives the onboarding and the customer must start a fresh one ([UC-ONB-23]). The resolver does not silently mutate an in-flight requirement set.

Rule: at most one in-flight onboarding per owner. An owner — an authenticated user, or an anonymous prospect token ([UC-ONB-28]) — cannot have two onboardings running concurrently; they complete or abandon the current one before starting another. This is why “your ongoing onboarding at step X” is always singular and unambiguous. Subscribing to a second offer ([UC-J-03]) is therefore sequential, not concurrent (do Livret, then ASV) — no multi-onboarding resume list is needed. Resume is keyed to the owner: an authenticated session resumes across devices; an anonymous session resumes only while its prospect-token cookie is held (losing it abandons the session, [UC-ONB-15]) unless the prospect authenticates and claims it ([UC-ONB-30]).

UC-ONB-04 — Confirmation / recap step

Onboarding always ends with a recap summarising the captured data for the customer to confirm — including data we pre-filled from what we already hold ([UC-ONB-10]), which is shown here even though it wasn’t asked as a Step. Some Steps are non-editable (a successful PVID id-check). Editing a field re-opens any dependent Step.

Correcting pre-filled data updates the source of truth. If the customer says a pre-filled value “is not right anymore, let me change it”, the correction does not stay local to this onboarding — it emits an update event that refreshes the canonical KYC / customer master data and its denormalized copies. So the recap doubles as a maintenance point for existing customer data. ⚠️ Corrections to regulated data (identity, the income bracket used for KYC) may require re-verification rather than a free edit — routed through the relevant compliance gate ([UC-REG-02]).

UC-ONB-05 — Officer validation → provisioning

A submitted onboarding goes to a compliance officer. On full validation the segment Provisioner creates only the entities that do not already exist — e.g. the physical person may already exist (existing customer), in which case it is reused, not duplicated — and then creates the missing account(s), card(s), and the subscription. The subscription period starts at validation. For an anonymous onboarding ([UC-ONB-28]) no user exists yet, so the Provisioner also creates the new user/login and promotes the session’s owner from the prospect token to it ([UC-ONB-29]).

UC-ONB-06 — Officer partial rejection → re-entry (one universal mechanism)

There is one mechanism for all re-entry, not a special rejection path. You open the app, you authenticate, the FE asks the backend, the backend says “you have an ongoing onboarding at step X”, and you are routed to the FE module for step X. The cause is irrelevant — it is the same flow whether you are:

A partial rejection simply marks the rejected data point(s) unmet (remediation injection); the resolver then surfaces them as the next gap(s), exactly like any other missing Step. Everything already captured stays in the DataBag; only the unmet Steps are asked again.

UC-ONB-07 — Officer full rejection (AML / fraud)

Officer rejects the whole onboarding for AML/fraud. Terminal; no account is provisioned. Downstream AML obligations are out of scope here (owned by compliance).

UC-ONB-08 — Per-step fraud signals (now specified: the onboarding event log)

Each Step emits behavioural signals (copy-paste, tab-switching, view dwell time, device) alongside server-observed context (IP, geolocation, ASN, device) that feed a future fraud score. No longer out of scope: this is specified as the onboarding event log in [UC-ONB-35], the PEP self-declaration in [UC-ONB-36], and surfaced to compliance/fraud in the back-office onboarding risk panel [UC-BO-12]. Sanctions/watchlist screening remains a separate compliance regime ([UC-REG-02]) and is out of scope for this build.

UC-ONB-09 — Free version of a paid product (ambassadors, employees)

An ambassador/employee gets a paid offer for free via the subscription-level free / employee modifier ([UC-SUB-29]) — not a cloned price-0 offer. The person onboards on the normal paid offer (same modules, same Steps); the modifier is set at activation, so the recurring charge is zeroed from the first invoice onward — they never pay, nothing to reimburse.

The modifier is determined before the first charge by the entry context: a whitelisted email/phone ([UC-OFF-12]) or a campaign code ([UC-ONB-24]) marks the subscription free/employee at creation. We do not maintain a parallel price-0 clone of every paid offer.

Distinction from a genuinely free offer ([UC-OFF-08]): a price-0 offer is free to a whole eligible cohort (e.g. the free plan); the free modifier makes an otherwise-paid offer free for a specific person. Use the offer when it’s free to everyone eligible; use the modifier for a per-person comp. (This supersedes the earlier “whitelist unlocks a 0-priced offer / onboard-then-reimburse” design — eliminating both the parallel catalogue and the refund.)

UC-ONB-10 — Onboarding from an existing account (pre-filled DataBag)

Design decision: requirements are constant per offer — we do not build a reduced requirement set for known customers. Instead the DataBag is pre-filled from the existing person, and the resolver (required − satisfied) naturally skips Steps whose data is already present and still valid. So an existing customer is asked only the genuine gaps; PVID is not re-run when a valid verification exists. Pre-fill applies only to a known identity (an authenticated user, or an anonymous session later claimed by one, [UC-ONB-28]/[UC-ONB-30]); a purely anonymous onboarding starts with an empty DataBag.

Staleness rule: data that is stale (expired id document, KYC refresh due) is excluded from the pre-fill DataBag, so the resolver treats it as a gap and the onboarding becomes the occasion to refresh it. Validity is judged per data point at pre-fill time. The exact validity windows per data point (id-doc expiry, KYC refresh cadence) — owned with compliance.

UC-ONB-11 — Minor / youth account (participant on the parent’s account)

Model: a youth account is the parent’s account with the minor as a participant — the parent is the holder and legal owner of the money; the minor is an authorized user ([UC-ONB-12]/[UC-SUB-26]) with a dedicated child app the parent enables (view balance + transactions) and a card with strict limits. This reuses the shared-account participant model — but as a constrained variant: the minor accesses the account only through the dedicated child app (never the main app) and under enforced age-appropriate limitations, so a minor-participant is not identical to a regular adult participant ([UC-ONB-12]). (Pixpay/Kard pattern.)

Onboarded from the parent’s account: the parent (an existing customer) adds the minor as a participant and provides the minor’s details — there is no separate minor-as-holder onboarding.

One parent suffices. Because it is the parent’s own account and own money, the parent acts in their own right; the second parent’s signature is not required, and the separated/divorced complication that affects a minor-as-holder account does not arise (French rule: one parent may perform actes usuels; both parents are needed only for a minor-held account’s actes de disposition — which this model avoids).

Limits & rights: the minor-participant’s card carries spending/withdrawal caps and feature restrictions set by the parent ([UC-SUB-26]); the parent can suspend/revoke access at any time.

At majority (18): no forced transition — the minor-specific constraints (child-app-only access, minor limits) lift, and the minor becomes a normal adult participant (a parent + adult-child shared account, [UC-SUB-20]). Nothing must move (the money was the parent’s); the adult may instead open their own account ([UC-ONB-01]), and the parent may revoke/transfer at will.

Trade-off (accepted): the child does not legally own the funds — fine for an allowance/spending product. A true child-owned account (gifts belong to the child, parent as administrateur légal) is a different, heavier product — out of scope for now.

UC-ONB-12 — Shared-account participant onboarding

A compte partagé is not a joint account: the account and the money belong to the primary holder; the second person is a participant who can act on the account and has their own card (matches the existing AccountHolder + AccountParticipant model). During the primary holder’s onboarding for this account, the primary provides the participant’s phone and email; the participant then runs their own onboarding (their identity/KYC) to be activated on the account. Decision: the account provisions and bills on the primary holder’s completion, independent of the participant — the participant activates on their own completion, or never. If the participant never onboards, the holder simply keeps paying for an unused shared account; that is acceptable and needs no special handling — the holder retains their withdrawal ([UC-BIL-14]) and closure ([UC-SUB-19]) rights if they no longer want it.

Tombstoned decision: Green-Got does not offer a true joint account (compte joint with co-ownership / solidarité). The participant has zero ownership and zero claim on the funds; the participant model is the only shared option. This is made explicit during onboarding — honest naming, not marketed as joint ownership — so there is no mis-selling. (Known product gap vs competitors who offer compte joint; accepted.) ⚠️ OPEN: participant card-issuance timing.

UC-ONB-13 — Company / pro onboarding (same crate, different Steps)

A company/pro offer uses the same commercial_domain/onboarding crate — there is one onboarding crate for all segments, not a generic engine with the steps extracted elsewhere. The B2B Steps (SIRET, KYB, BO declaration) and the retail Steps live in the same registry, each gated by applies_to(offer, bag) so the resolver only surfaces the ones relevant to the chosen offer. The existing business_domain/onboarding scaffolding moves into commercial_domain/onboarding. This crate is the cross-domain orchestrator: it depends on the domains it provisions into (organisation for B2B; core_banking / physical_person / investment for retail) — intentional, because onboarding is where the segments meet. (The other catalog crates — offers/subscriptions/billing/accounting — stay segment-neutral data + logic.)

UC-ONB-14 — KYC failure

Identity verification fails (provider reject / mismatch). The onboarding cannot complete; the customer may retry per policy; no provisioning. Distinct from officer rejection.

UC-ONB-15 — Abandoned onboarding (TTL) & archival cleanup

An onboarding left incomplete expires after the TTL (defined in the offer) and is archived: the session row and its DataBag are retained, not deleted (terminal Expired/Abandoned). Re-entry starts a fresh session (DataBag may re-pre-fill from any persisted, non-anonymised person). Retention, anonymisation, and legal hold then follow the purpose-based model in [UC-REG-08].

Archival cleanup (hard-delete contract). An abandoned/expired onboarding never provisioned an account, so the transient identity bootstrap it created is hard-deleted at archival — the throwaway app_user, its sessions, OTP attempts, any credential, and the email/phone login identifiers. This leaves no half-formed login behind and frees the unique email/phone so the person can re-onboard. Fraud signals are kept for later investigation: KYC identity verifications, any physical person, and the session’s DataBag (the KYC retention obligation under [UC-REG-08] attaches to those kept signals, not to the throwaway login). Two guards bound the delete: it only touches a user this onboarding itself minted ([UC-ONB-31] promotion; never a pre-existing authenticated user), and never a user who has since become a real customer (validated any onboarding, [UC-ONB-05]).

Classification rule (maintained invariant). Every onboarding Step that persists data outside the DataBag must classify each artifact TRANSIENT (hard-deleted at archival) or FRAUD SIGNAL (retained, with reason). Archival cleanup is an explicit allowlist — unclassified data is never auto-deleted — and the app_user foreign keys are the loud-fail backstop: a new durable user-owned artifact that forgets to classify aborts the archive transaction rather than silently leaking or losing data. Implemented in the onboarding crate use_cases/archive.rs; lifecycle in 4_onboarding_lifecycle_and_status.md §2.6.

UC-ONB-16 — Re-onboarding after closure

A former customer (account closed) re-onboards. Decision: pre-fill with everything we still hold that is not stale ([UC-ONB-10] staleness rule). Subject to the retention/anonymisation model ([UC-REG-08]) — i.e. unless the holder’s profile PII has been anonymised after the 10-year window — pre-fill has everything minus stale items; the onboarding refreshes whatever is stale or missing. An anonymised former holder is treated as a new prospect.

UC-ONB-17 — Cross-device resume

The general resume principle ([UC-ONB-03]) seen from another device: state lives server-side, so a device switch works for free with no data loss — the new device just renders the backend-named Step.

UC-ONB-18 — Subscribable-offers for an existing customer (collision-aware)

The route excludes offers that would duplicate something the customer already holds: where a module is one-per-person ([UC-SUB-11]) — e.g. the payment-account/current-account module — a second such offer is not offered, and the upgrade (an offer change on the existing subscription) is offered instead ([UC-SUB-12]). Genuinely different products (shared, livret, ASV) remain offered. The exclusion is derived from module specs ([UC-OFF-14]) — each module declares a category + per-person cap, and the gate excludes an offer whose module category the customer already holds one-per-person; there is no separate exclusion matrix. Visibility rules also apply ([UC-OFF-12]): restricted offers appear only for whitelisted identities.

UC-ONB-19 — Event onboarding via a pre-created inactive card (QR deeplink)

Cards can be pre-created, inactive, linked to nothing, and handed out physically at live events: “scan this card to open your account; if accepted, Green-Got credits €50 as part of the offer.” The card has no balance, is not usable, and is not a payment instrument for the customer until KYC is completed and an account is created. Each card’s paper carries a QR code that uniquely identifies the pre-created inactive card and deeplinks into a special onboarding bound to a specific event offer. Scanning the QR starts (backend-created, [UC-ONB-02]) an onboarding for that offer; on validation the pre-created card is linked to the newly provisioned account and Green-Got credits the account with the promised €50 by internal transfer from its functioning / marketing-funded account. The credit is reason-coded and treated as a customer-credit liability / pending balance until settled into the customer’s payment-account position ([UC-BIL-16], [UC-ACC-06]). This is a specific offer with a specific onboarding (its own Steps). The €50 acquisition credit is a regulated acquisition premium ([UC-REG-10]); its funding & approval follow the campaign-code rules ([UC-ONB-24]/[UC-REG-10]).

Uniqueness & security (resolved). Each card ships with paper carrying a unique QR + code that identifies that specific card; activation requires an NFC tap of the physical card against the phone, which reads the card’s unique, secure chip ID — so a copied or cloned QR alone cannot activate an account (you must possess the physical card).

TTL (resolved). A pre-created inactive card has no TTL and is never reclaimed or voided on a timer: it stays activatable for as long as the system lives — as long as activation is possible, the card lives, and its bound event offer stays attached. Activation is the only state change: on activation (QR + NFC tap) and onboarding validation, the card is assigned its PAN, expiry date and real card lifecycle and is linked to the newly provisioned account. There is no unclaimed-card sweep.

UC-ONB-20 — Paid onboarding top-up before the payment account exists

For paid offers that require an initial top-up, the customer sends funds to Green-Got before their payment account exists. This creates a pending onboarding top-up, not a customer-account balance, linked to the onboarding id, funding reference, payer identity, and compliance status. The top-up is credited to the customer’s payment-account sub-ledger only after KYC/compliance validation and account provisioning. Until then it cannot be spent or used to activate a card.

Settlement reality & prefunding. A card top-up is authorized instantly by the acquirer (Stripe) but settles over days through the rails: old bank → Mastercard settlement bank → Stripe → Green-Got’s functioning / operating account → the customer’s account. We do not make the customer wait for settlement: on a confirmed authorization plus KYC/compliance validation, we provision the account and credit usable funds immediately, prefunded from Green-Got’s functioning account — so a customer who onboards, tops up, and is validated within minutes has spendable money at once.

The prefunded amount is safeguarded from Green-Got’s own funds until the acquirer settlement lands and reconciles (full D+1 / shortfall mechanics in [UC-REG-09]). Prefunding is gated on the authorization (instant, high settlement confidence), not on awaited settlement — which bounds the risk; a top-up that later fails or reverses after prefunding is the recovery case ([UC-ONB-21]): it becomes a receivable to recover, not a customer-visible failure. The settlement lag is abstracted from the customer.

UC-ONB-21 — Top-up not received / failed / mismatched

If the expected top-up is not received, is received for the wrong amount, or cannot be matched to the onboarding/payer, the resolver keeps the top-up Step unmet and surfaces remediation. The onboarding cannot be submitted for final validation until the funding gap is resolved or the offer path is changed to one that does not require the top-up.

UC-ONB-22 — Onboarding rejected, expired, or suspicious after funds were received

If funds were received but onboarding does not activate:

This refund is return of pending onboarding funds, not a subscription refund.

UC-ONB-23 — Offer retired or Step set changed during onboarding

If the offer is retired, or the offer’s onboarding Step set changes, every in-flight onboarding for that offer version is archived: it remains auditable but the customer cannot access or resume it. The customer must start a fresh onboarding on a currently subscribable offer/version. If the last customer-provided information is less than 7 days old, notify the customer; otherwise archive silently. Any pending top-up follows [UC-ONB-22].

UC-ONB-24 — Campaign code: lifecycle & config

A campaign code ([UC-ONB-01]) is a back-office-defined object carrying a config plus lifecycle controls:

Two entry modes. A code can be (a) entered as an onboarding Step — a “got a code?” Step inside a flow that, when valid, attaches its config (promo / prime / whitelist) to the running onboarding; or (b) act as a gate to a specific offer’s onboarding — the code unlocks/launches that offer’s flow at the gate ([UC-ONB-01]). The same code object serves both.

When a code is not-yet-started, expired, or exhausted, it simply does nothing — the prospect falls back to the default subscribable-offers gate ([UC-ONB-01]) with a clear “this code is no longer valid” message; it never blocks normal onboarding. The redemption count and validity are enforced on the backend at use, and the “first N” cap must be race-safe (no over-issue beyond the cap).

Examples:

UC-ONB-25 — Validation: compliance-officer always (migration excepted)

A submitted onboarding is validated before provisioning ([UC-ONB-05]). Every onboarding is finalized by a compliance officer — there is no automatic validation of a real onboarding, regardless of plan (free or paid), segment (retail or B2B), or whether the customer is first-time or returning. The sole automated exception is the agent-PI → independent-PI customer-base migration ([UC-MIG-03]), which is mechanically an automated onboarding built from existing, already-KYC’d legacy data and is therefore validated automatically.

Automatic gate checks ([UC-REG-02]) still run as a pre-screen, but a human officer owns the final decision for every non-migration onboarding; any gate hit escalates to an officer.

UC-ONB-26 — DataBag vs canonical customer/KYC data

The DataBag is the transient working state of one onboarding — what’s been collected for this offer. It is not the source of truth: it is hydrated from the canonical customer / KYC master record (pre-fill, [UC-ONB-10]) and, on validation, flushed to it (provisioning writes the canonical person/account, [UC-ONB-05]). Corrections to pre-filled data at recap update the canonical record via an event ([UC-ONB-04]). The DataBag is archived/expired with the onboarding ([UC-ONB-15]); the canonical record persists under the retention model ([UC-REG-08]). An anonymous onboarding ([UC-ONB-28]) has no canonical record to hydrate from — its DataBag begins empty and is flushed to a newly created customer record at validation ([UC-ONB-29]).

UC-ONB-27 — Provisioning saga & idempotency (partial-failure recovery)

On validation, provisioning creates several entities across domains (person reuse, account, card, subscription, first invoice + collection — [UC-J-01]); it is a multi-step saga that can partially fail (e.g. account created, card issuance pending). Requirements: each step is idempotent (safe to retry; no duplicate accounts/charges — [UC-ONB-05] “create only what doesn’t exist”); the saga is resumable and converges, or surfaces a partial-provisioning dead-end to the BO for manual recovery ([UC-BO-08]). The provision-before-charge order is fixed ([UC-J-01]): account + top-up before invoice + collection. ⚠️ OPEN: compensation/rollback policy when a downstream step is unrecoverable.

UC-ONB-28 — Onboarding owner: anonymous prospect or authenticated user

An onboarding session is owned by exactly one identity, fixed at creation, of one of two kinds:

Authorization (generalised ownership rule). Every read / resume / submit / edit / submit-for-review is authorised by matching the caller’s identity to the session’s owner — the prospect-token cookie for anonymous, user_id for authenticated. A mismatch is treated as not found (no existence disclosure). The one-in-flight rule ([UC-ONB-03]) is per owner (per prospect token, or per user). Resume: authenticated sessions resume across devices (server-side, keyed to the user); anonymous sessions resume only while the prospect-token cookie is held — losing it abandons the session ([UC-ONB-15]); cross-device anonymous resume needs the prospect to authenticate and claim it ([UC-ONB-30]). The subscribable-offers gate itself is unauthenticated ([UC-ONB-01]); the same Steps, registry, and resolver serve both owner kinds — only the identity binding and pre-fill differ.

UC-ONB-29 — Promotion of an anonymous onboarding to a real customer

Promotion now happens in two distinct moments, not one:

  1. Auth-identity promotion (at get_email). The thin user + verified email/phone login-identifiers and the authenticated session are created when the contact Steps verify ([UC-ONB-31]). From here the onboarding owner is the user_id; the prospect token is retired.
  2. Customer provisioning (at validation). Provisioning ([UC-ONB-05]/[UC-ONB-27]) remains the moment the prospect becomes a customer: the Provisioner reuses the already-created user and creates the person, account(s), card(s) and subscription from the validated DataBag. No customer-facing entity is created before validation (mirrors the migration reversibility principle, [UC-MIG-03m]).

For an authenticated onboarding (existing customer, [UC-J-03]) the user already exists and is reused ([UC-ONB-05]); only the missing entities are created. After validation the customer continues authenticated and sees the post-submit pending state ([UC-J-01]).

Credential bootstrapping — resolved. The former open item (how a brand-new prospect first authenticates) is answered: the prospect authenticates passwordlessly via the email/phone OTP during get_email/get_phone, which both creates the durable login-identifiers and mints the session ([UC-ONB-31]). A first-class password / passkey credential remains a later, optional enrolment owned with the authentication domain, but it is no longer a blocker for the prospect to hold an authenticated session through onboarding.

UC-ONB-30 — Sign-in during an anonymous onboarding (claim / merge)

A prospect running an anonymous onboarding ([UC-ONB-28]) may authenticate mid-flow (they turn out to be an existing customer, or they create a login). Decision: the anonymous session is claimed by that user — its owner switches from the prospect token to the user_id, and the DataBag is enriched with canonical pre-fill ([UC-ONB-10]) for any still-missing, non-stale points — only if the user has no other in-flight onboarding. If the user already has one in flight ([UC-ONB-03] one-in-flight), the two are not silently merged → route to customer support ([UC-BO-08]). This preserves the one-in-flight-per-owner invariant across the identity transition.

Anti-abuse — exact identity match required (decided). A claim is allowed only if the identity already captured in the anonymous DataBag matches the authenticating user’s canonical identity — the collected name and date of birth must equal the signing-in user’s. On any mismatch the claim is refused: the anonymous session is not attached to the user (route to customer support ([UC-BO-08]), and the anonymous DataBag is discarded rather than grafted onto the account). This stops a prospect from collecting (or PVID-verifying, [UC-ONB-04]) one person’s identity anonymously and then attaching it to a different signed-in account ([UC-REG-02]). The match is exact by default; any future loosening (e.g. fuzzy match + re-verification through the compliance gate) is a compliance decision, not a silent relaxation.

UC-ONB-31 — First steps for an unknown prospect: verify email then phone → authenticated session

For an onboarding started from the logged-out surface ([UC-ONB-01]), onboarding is create-first: starting immediately creates a prospect-owned session (an empty DataBag, a freshly minted prospect token, and the onboarding id returned to the app so it can persist it and resume). The prospect token is delivered per surface: an httpOnly cookie on business web, and in the response body on retail mobile (which can’t read the cookie), echoed back in the x-onboarding-prospect header.

Segment note. The description below is the business (email-first) path. The order is snapshot-driven, so retail is phone-first, and its mint fires when the whole bootstrap set completes — email + phone and the device key registered at secure_device — not at get_email. The retail POC step order and the mobile contract are in 10. Retail Mobile Integration; the retail catalogue also defaults the start offer (free) and country (FR).

The unauthenticated path then runs two verified contact Steps, in order:

B2B password variant — get_email_password. The sole-trader (B2B web) flow opens with get_email_password in place of get_email: the same verified-email + session-mint Step, but the issue phase also takes a password (+ confirmation) and the mint additionally creates a durable password credential, so the B2B customer can later log in with email + password (a deliberately lower-security method, expected to be dropped eventually). Retail keeps the passwordless get_email. The password is argon2-hashed inside the Step and held only as a secret transient bag marker until the mint consumes it — never logged ([UC-ONB-35]) nor shown in the back office; archival deletes the credential with the rest of the transient identity ([UC-ONB-15]).

These two Steps replace the former single contact_details step (email+phone, unverified) and the former separate mobile_otp step — phone collection and verification are now folded into get_phone. The DataBag is seeded only with the verified contact points and otherwise starts empty ([UC-ONB-10], [UC-ONB-26]). An authenticated onboarding skips both Steps — identity is already known and verified ([UC-ONB-28]).

Scope note. The early auth identity is deliberately thin: a user plus its verified email/phone login-identifiers and a session — and nothing else. No person, account, card, subscription or provisioned customer exists until validation ([UC-ONB-29]); the session therefore still grants no access to real customer data (there is none yet). This narrows but preserves the “nothing of value is created until validation” principle: only the authentication shell is created up front, so the prospect can carry an ordinary authenticated session through the rest of onboarding and resume across devices.

UC-ONB-32 — Duplicate identity at a contact Step: enumeration-safe (existence signal goes to the channel, not the screen)

The duplicate-identity check runs per contact Step: the email at get_email and the phone at get_phone ([UC-ONB-31]). When the submitted email/phone already belongs to an existing login-identifier, the backend still creates no user/identifier/session and does not advance the Stepone account per identity at the entry is enforced.

Decision — the on-screen response is invariant; the “you already have an account” signal is delivered only through the contact channel the real owner controls. The person at the keyboard sees the same response in both cases“we’ve sent a code to your email/phone, enter it to continue” — same wording, same next screen, comparable timing. What differs lands only in the inbox/phone:

Because a stranger probing addresses they don’t control sees an identical screen every time, the flow does not leak account existence (enumeration). The differentiation happens behind the contact-channel authentication boundary, not on a public screen. This is the not-yet-authenticated counterpart of the sign-in claim ([UC-ONB-30]).

Required to actually hold the property (not just the copy): (a) timing parity — an email/SMS is sent in both branches, so neither path is measurably faster; (b) rate-limit + CAPTCHA the contact Step so the “send to anyone” primitive can’t be used for bulk probing or to spam victims; (c) consistency across every entry point — login and credential-recovery must not reveal existence either, or an attacker just uses the weakest door ([UC-REG-02]). Trade-off accepted: even brand-new users must go check their channel (no on-screen “you’re new, keep going” shortcut). This supersedes the earlier on-screen “this credential already exists — please log in” outcome (which leaked existence).

UC-ONB-33 — Request channel (web vs mobile) drives phone verification + session lifetime

Onboarding is otherwise channel-agnostic, but the get_phone Step and the minted session need to know whether the caller is the web app or the native mobile app. A channel (web | mobile) is carried on the contact-Step submissions (defaulting to web). It selects (a) the phone-verification provider — web uses Vonage OTP, mobile uses Infobip silent network authentication (deferred) — behind a single provider trait, and (b) the session device type, which sets the session lifetime (short, sliding web session vs. long-lived mobile session) when get_email mints the session ([UC-ONB-31]). For this iteration only the web/Vonage path is implemented and Vonage + the email sender are stubbed; the mobile/Infobip adapter is a typed seam to be wired later.

UC-ONB-34 — Confirm offer: a confirm_offer Step that can switch to a sibling

Every offer in a variant group ([UC-OFF-15]) includes a confirm_offer Step placed immediately before recap. It shows the prospect the offer they are onboarding for plus its siblings with their prices, and lets them switch the onboarding to a sibling before final review. Selecting the current offer is a no-op confirmation; selecting a sibling changes onboarding_session.offer_id to it. Because siblings share an identical Step set ([UC-OFF-15]), the change keeps every already-captured DataPoint and the snapshotted required_steps valid — nothing is re-asked or lost. The Step provides OfferConfirmed; the engine validates the chosen offer is the current offer or one of its siblings (otherwise a field-level error), persists the offer change atomically with the Step, then advances to recap. The chosen offer is the one finally subscribed at validation ([UC-ONB-05]). An offer with no siblings still shows the Step as a plain confirmation; a non-grouped offer that omits the Step is unaffected.

UC-ONB-35 — Onboarding event log (server + client risk signals)

Every meaningful onboarding interaction is recorded as an immutable event so compliance and fraud can reconstruct what happened, when, from where, on what device — the concrete realisation of the per-step fraud signals reserved in [UC-ONB-08]. Events are append-only rows on a dedicated onboarding_event table (not the transient DataBag), keyed to the onboarding id and ordered by a per-onboarding sequence number.

Two signal sources per event.

Purpose & non-goals. The log’s purpose is evidentiary + future scoring: today it is human-read in the back office ([UC-BO-12]); the eventual goal is an automated onboarding risk score computed from the accumulated meta. Scoring itself is out of scope for this build. Events are Postgres rows now; a later mirror to the analytics warehouse (ClickHouse, via the event bus) is the scoring seam and does not change this contract. Events follow the same retention/anonymisation model as the session ([UC-ONB-15]/[UC-REG-08]).

UC-ONB-36 — PEP self-declaration Step

A pep_declaration Step captures the customer’s politically-exposed-person status as a regulated self-declaration, feeding the ongoing PEP/adverse-media compliance gate ([UC-REG-02]). It is a full declaration, not a single boolean: is-PEP (yes/no) and, when yes, the category / public function held, plus family-member-of-a-PEP and close-associate-of-a-PEP flags. The Step provides these as DataPoints and is required by every offer (retail and sole-trader), placed immediately after identity verification (id_doc_pvid) alongside the other regulated Steps; it is a shared dictionary entry ([UC-ONB-02]). The declaration is surfaced to the compliance officer at review ([UC-BO-12]) and is one of the facts the officer weighs at validation ([UC-ONB-05]); a positive declaration does not auto-reject — it routes to enhanced due diligence per compliance policy ([UC-REG-02]). Screening the declared identity against sanctions/PEP watchlists (OFAC, UN, EU) is the separate screening regime ([UC-REG-02]) and is out of scope here — the back office shows it as a not-yet-wired placeholder ([UC-BO-12]).

UC-ONB-37 — Login-time onboarding gate

A user only exists once an onboarding minted them ([UC-ONB-31]), and nothing of value (person, account, card) exists until validation ([UC-ONB-29]). So when a user logs in, what they may do is determined by where their onboarding stands, and the app enforces it. The backend classifies the current user (their single in-flight onboarding, if any, plus whether they have ever validated one) into a gate carried on the session-bootstrap (/me) response:

The gate is derived, not stored (from onboarding status + the has-ever-validated signal), and it is a UX/authorization convenience on top of the status machine (4. Lifecycle) — it grants no new rights and never bypasses officer validation ([UC-ONB-25]).


3. Offers & catalogue (offers)

UC-OFF-01 — Unit offer (1 module)

An offer composing a single module (e.g. Essential payment account / current-account module). The canonical sellable.

UC-OFF-02 — Bundle offer (n modules, one price)

An offer composing several modules at one discounted price (e.g. Valentine: current + shared — a discount on the shared account). A bundle is one offerone subscription with multiple modules ([UC-SUB-04]). Savings (livret/ASV/PER) are never priced bundle modules; holding one is instead an entry-eligibility discount on a paid offer ([UC-OFF-04]) — e.g. holding an ASV grants a discount on Premium — leaving the savings product itself free and commission-funded ([UC-SUB-15]).

UC-OFF-03 — Time-phased promo price (intrinsic to the offer)

An offer can carry a price schedule (e.g. €10/mo for 12 months, then €13). The promo is part of the offer, not a separate stackable discount object. On month 13 it auto-reverts; no new subscription. Customer-specific discounts and free months (promo codes, parrainage, comp) are a different thing — they live on the subscription and apply at the invoicing layer ([UC-SUB-29], [UC-BIL-19]), not on the offer.

UC-OFF-04 — Eligibility is continuous (re-checked every period)

All commercial eligibility conditions are continuous. They are evaluated at subscribe/change time and re-evaluated at every subscription period (billing cycle). If a condition no longer holds at a period boundary, the subscription transitions to its declared fallback offer ([UC-OFF-11]) from the next period — with advance notice, perks kept to the boundary, and no commercial refund ([UC-SUB-07]). Checks happen at period boundaries, never instantaneously, so incidental intra-period state changes never disrupt a live subscription; and every conditional offer must declare a fallback ([UC-OFF-13]) so the transition is always clean. This is what keeps continuous eligibility from becoming the “nightmare” of arbitrary instant re-pricing.

Example (bundle only). A conditional discount lives inside a bundle, never as a standalone read-once gate: e.g. a bundle whose shared account is 50% off because the holder also holds the individual payment account is re-checked each period — every period we verify the individual account is still held; if it is gone, the discount/bundle falls back (the shared account reverts to its standard unit offer, [UC-SUB-05]) from the next period.

Deterministic boundaries (age, a fixed date) are the sub-case that can be scheduled and notified in advance (e.g. a fixed-term promotional offer’s end date, [UC-SUB-20]); arbitrary conditions (“holds X”, “balance ≥ Y”) are caught at the next period boundary. Both resolve through the same declared-fallback mechanism ([UC-OFF-11]). Gaming note: an instant-state gate read at one boundary (“balance ≥ Y”) can be gamed across the period — prefer conditions evaluated over a sustained window, or accept the gaming risk knowingly.

This governs commercial eligibility. Regulatory/compliance gates (sanctions, KYC/KYB freshness, PEP/adverse-media review, tax/regulatory residence, legal capacity, licence perimeter, partner eligibility) are a separate, continuously/periodically rechecked regime under [UC-REG-02].

UC-OFF-05 — Offer lifecycle: start / end dates & retirement

Every offer has a start date and an optional end date. It is subscribable only within [start, end]; after the end date it is retired and no longer subscribable. Existing subscriptions on a retired offer do not close (grandfathering); they migrate only on an explicit event ([UC-SUB-17], [UC-OFF-11]). A retired offer must still resolve to an active fallback for any forced migration ([UC-OFF-13]).

UC-OFF-06 — Versioning / immutability (material vs editable fields)

An offer/module has two kinds of field:

The test: does the change alter what the customer gets or pays (price, composition, a contractual entitlement)? Yes → material → new version. No → presentation → in-place edit. ⚠️ Borderline cases (a real entitlement vs merely its description) must be classified per this test; when in doubt, treat as material.

UC-OFF-07 — Per-module standard base offer (fallback)

Every module maps to a standard unit offer used as the fallback target on bundle dismantle ([UC-SUB-05]). The fallback is derived per module, not stored per offer.

UC-OFF-08 — Free offer (price 0, but still fee-capable)

An offer with a 0 recurring price (free plan, or free version of a paid product). Billing behaviour — no recurring invoice but still fee-capable for one-off events — is in [UC-BIL-13].

UC-OFF-09 — Upgrade/downgrade relationship metadata

An offer lists the comparable offers that are upgrades/downgrades of it (for offers covering similar modules), so the system can classify a switch as immediate (upgrade/lateral) vs deferred (downgrade). ([UC-SUB-06]/[UC-SUB-07]/[UC-SUB-08]). Classification is on the offers’ intrinsic gross price + rights (the catalogue relationship), not on the customer’s effective payable amount: customer-specific modifiers (cadence, free-month counter, employee tag, promo, VAT/country — [UC-SUB-29]) are applied after classification at the invoicing layer and never reclassify a switch. This keeps the B2 “an upgrade is always strictly more expensive” invariant ([UC-SUB-06]) well-defined.

UC-OFF-10 — Cross-generation offers never conflict

Valentine 2027 and Valentine 2028 are independent immutable rows; because pricing is intrinsic and snapshotted (no stacking), they cannot conflict. Continuing-eligibility transitions ([UC-OFF-11]) also avoid conflict because each offer resolves to its own declared fallback, not to another generation’s offer. This is a property to prove, not just assert.

UC-OFF-11 — Eligibility fallback (lifecycle transition)

Any offer carrying an eligibility condition ([UC-OFF-04]) must declare: the condition, its boundary (a deterministic date/age, or simply the first period boundary at which the condition no longer holds), the fallback offer to move to, and the timing (at the boundary, or the next cycle after). When the boundary is reached the system performs a scheduled offer change (close-then-open, [UC-SUB-20]) to the fallback — never a silent in-place mutation, and no commercial refund unless a legal/correction path applies. The customer is notified in advance. If the fallback cannot be applied with data we already hold (e.g. minor→adult needs full adult KYC and a new payment account), the transition is a triggered re-onboarding rather than a silent migration ([UC-SUB-20]).

UC-OFF-12 — Offer visibility & restricted access (whitelist)

Every offer carries a visibility flag — e.g. publicly_accessible (default true):

A paid closed-beta / invite-only offer is the canonical restricted offer — restriction is about visibility, independent of price (employee/ambassador free is now a subscription modifier, not a restricted price-0 offer — [UC-ONB-09]). Decisions: whitelist granularity is per offer; it is managed in the back office by customer-care officers. The match may be on a claimed (not pre-verified) email/phone — abuse is caught because every account is manually validated by a compliance officer ([UC-ONB-05]): an impersonator (e.g. pretending to be an employee of a 50-person company to grab a free/employee comp or a restricted offer) is caught fast at validation. No hard verified-identity gate is required on the whitelist itself.

UC-OFF-13 — Fallback integrity (continuous verification)

Offer switches (age-out, condition-no-longer-met, retirement, bundle dismantle) all need a target to fall back to, so an offer that can be force-migrated must resolve to an active fallback at all times. Primitive offers (e.g. the standard Essential payment account) are self-standing and need no fallback — they are the terminal fallback targets. The system must continuously verify (startup assertion + periodic check) that every non-primitive offer that may require a forced migration resolves to an active fallback — mirroring the onboarding engine’s registry-coverage assertion. A broken chain (fallback retired with no replacement) is an operational alarm, surfaced before it can strand a customer. If falling back would increase the customer’s price or materially change the framework contract, the transition must go through [UC-REG-01] notice/consent/rejection rules; fallback integrity proves a target exists, not that a silent price increase is allowed. Decision: a primitive is marked by fallback = self (it is its own terminal fallback); an explicit is_primitive flag may also be carried for clarity. A broken chain alerts customer support, who resolve it in the back office ([UC-BO-08]) before it can strand a customer.

UC-OFF-14 — Module catalogue (shared dictionary + per-offer composition)

The mirror of the Step dictionary ([UC-ONB-02]) for the “what you get” axis. There is a single catalogue (dictionary) of reusable Modules — the Bibliothèque de modules — each defined once: payment_account (tier Essential/Premium), shared_account (account + participant capability), card (sub-module: physical/virtual, Standard/Premium), livret, assurance_vie, per. Every offer composes a subset of the catalogue; it never invents inline modules. The same Module is the same catalogue entry everywhere, and a subscription freezes the module version it references ([UC-SUB-32]).

Each Module spec carries the rules — there is no separate matrix, exclusions/dependencies are derived from the specs:

The subscribable-offers exclusion ([UC-ONB-18]) falls out of this: the gate compares an offer’s modules’ categories/caps against the customer’s held modules; a category already held one-per-person → the offer is excluded and the upgrade offered instead. No standalone exclusion matrix is maintained.

UC-OFF-15 — Sibling offer groups (variants sharing one onboarding)

An offer may belong to a variant group, identified by a shared group_code: a set of offers that are the same product at different price/perk points — e.g. fr_sole_trader (9€/mo) and fr_sole_trader_premium (15€/mo) share group fr_sole_trader. Group members are siblings. Hard invariant — siblings MUST declare an identical ordered set of required onboarding Steps (enforced by a catalogue assertion/test). This is exactly what makes switching between siblings safe mid-onboarding ([UC-ONB-34]): because the Step set is identical, the session’s snapshotted required_steps stays valid when the offer changes, so nothing is lost or re-asked. Siblings typically differ only in price_cents, name, composed modules/perks and is_primitive (the base variant is primitive; richer ones are not). The back-office offers list shows each variant as its own row; the group is the group_code they share.

Groups exist per scope ([UC-ONB-01]): the business scope has the fr_sole_trader group (base + premium, whose onboarding includes siret_lookup), and the retail scope has a retail group — retail essentials (base) + retail premium — whose onboarding is the same minus siret_lookup (retail is not a company; siret_lookup.applies_to is business-only anyway). Both retail variants leave available_legal_forms empty (unrestricted) and are segment = Retail.


4. Subscriptions & lifecycle (subscriptions)

UC-SUB-01 — Single individual account (canonical)

S1 = (P, Essential, {current_account, card}, active).

UC-SUB-02 — Individual + shared (two subscriptions)

S1 individual, S2 shared — separate accounts → separate subscriptions ([UC-SUB-12]). A shared account is the primary holder’s account with a participant ([UC-ONB-12]), not a co-owned account.

UC-SUB-03 — Individual + shared + ASV (à la carte)

Three subscriptions, each at catalogue price; ASV references an investment contract and bills nothing ([UC-SUB-15]).

UC-SUB-04 — Bundle = one subscription, multiple modules

S = (P, Valentine bundle, {current, shared}, active) — one subscription, one price.

UC-SUB-05 — Bundle dismantle / explosion

A module inside a bundle is closed (e.g. the shared account). The bundle can’t stand → the bundle subscription closes; surviving modules fall back to their unit offers at standard price via re-subscription / offer change ([UC-OFF-07]). Net: holder pays the sum of unit offers (more than the bundle); Green-Got does not keep providing a surviving module for free after the bundle predicate disappeared.

Customer-initiated dismantle uses explicit consequence confirmation: before accepting the module closure, the UI/BO shows the surviving modules, their fallback offers, the next price, and the effective date. The customer can either confirm the new price, keep the bundle until period end, or close the surviving modules too. If the dismantle is forced by legal/compliance/product impossibility, the system schedules the fallback and applies [UC-REG-01] notice/rejection rules when the customer price or framework terms materially increase. BO-doable ([UC-BO-02]). The inverse — merging unit subscriptions back into a cheaper bundle — is [UC-SUB-21].

UC-SUB-06 — Upgrade (price ↑), mid-period — immediate, pro-rata

Close-then-open; new perks immediately; delta billed daily pro-rata on gross ([UC-BIL-08]). Invariant: an upgrade’s target offer is always strictly more expensive than the source — this is guaranteed at offer-definition time by whoever manages the catalogue ([UC-OFF-09]). The system asserts the netted upgrade invoice is ≥ 0 ([UC-BIL-08]) and refuses the move otherwise: an “upgrade” that would net negative is misclassified, not an upgrade.

No free-upgrade path. Because perks activate immediately but fees collect only by internal transfer from the balance ([UC-BIL-05]) and the account is never pushed negative for our own fee ([UC-SUB-14]), the upgrade is applied only if its netted invoice can be collected at the switch (balance ≥ amount). Otherwise the upgrade is declined (the customer stays on the current offer) — premium perks are never granted ahead of collection. (This differs from a recurring fee, which is allowed to fall into arrears.) Insufficient balance does not make the upgrade non-immediate — it makes it impossible; there is no deferred-upgrade state. Upgrades are immediate when collectable, full stop.

UC-SUB-07 — Downgrade (price ↓) — end of paid period, no commercial refund

Current subscription runs to cycle end with current perks kept; the cheaper subscription starts next cycle. No commercial proration / unused-time refund because perks were retained until the effective boundary; legally mandatory corrections still route through [UC-BIL-09]. A later upgrade cancels the scheduled downgrade ([UC-SUB-10]).

UC-SUB-08 — Lateral move (same price)

Immediate, treated like an upgrade; nothing owed back.

UC-SUB-09 — Annual ↔ Monthly

“Annual” is a conditional discount, not a no-exit lock: the bare payment-account framework contract is always resignable at will (Payment Accounts Directive / CMF L.314-13). A true “12 months, no exit, no refund” lock on a payment account is a clause abusive and is not used. Commitment may attach only to the wrapper layer (savings/insurance), never the bare payment account inside a bundle.

The annual price is realised as one of:

Monthly → Annual = immediate (commitment upgrade). Annual → Monthly = end of the already-paid year. Either way early termination forfeits the discount, never access or money. Statutory termination/pro-rata rights override via [UC-REG-01]/[UC-BIL-09].

UC-SUB-10 — Only one pending change; latest wins

Since upgrades are immediate, only a downgrade can sit pending; a later upgrade cancels it.

UC-SUB-11 — Inter-module dependencies

All investment/savings products (livret, ASV, PER) require an active payment account. That payment account may be a free one — and a customer who doesn’t want to actively use it can hold a free account with only a virtual card (no physical card). Shared does not require an individual account. Cards require an account (card = sub-module of an account, [UC-SUB-16]).

Green-Got product caps (e.g. at most one PER, one PEE, one livret of a kind per person at Green-Got) are declared in the module spec as per-person uniqueness within Green-Got and enforced at subscribe time. This is a business rule, not a legal cap — a person may legally hold several PERs (PER individuel + collectif + obligatoire, even multiple individuels), and may hold the same product at another bank; we simply don’t let them hold two of the same inside Green-Got because it serves no purpose. We only check our own holdings (which we fully know); holdings elsewhere are irrelevant to us. (The true legal one-per-person uniqueness is Livret A / LDDS / LEP — and even that can’t be hard-gated at subscribe time since only FICOBA knows about livrets held elsewhere; that constraint, if ever in scope, would be declaration-based/partner-dependent, not a hard gate.) This is a person-level cap, distinct from the account↔subscription binding ([UC-SUB-12]).

UC-SUB-12 — One subscription per account; changes are offer changes

Invariant: each account (active IBAN) is bound to exactly one active subscription — an IBAN exists in only one subscription. (A bundle subscription can cover several accounts/modules, but each of those accounts still belongs to that single subscription.) Therefore changing what an account is subscribed to is never a second subscription on the same account — it is an offer change (close-then-open) on that account’s subscription ([UC-SUB-06]/[UC-SUB-07]).

So when a holder who already has a payment account picks an offer that would change that account’s plan (Essential → Premium), the system routes it to an offer change on the existing subscription, not a new one. A genuinely different product (shared, livret, ASV) is a separate account → separate subscription, which is fine. Whether a second account of the same kind is allowed (e.g. a second payment-account/current-account module) is governed by per-person module caps ([UC-SUB-11]); where the module is one-per-person, the second offer is not offered and the customer is steered to an offer change / upgrade instead.

UC-SUB-13 — Individual → shared transformation (support-only exception)

This is not a general self-service path — it is an exception handled by customer support (too complex to self-serve). Customer support migrates the account to a shared-account subscription (an offer change to the shared offer) on the existing account — the account and money stay with the primary holder; no new account is provisioned, the account is not replaced — and invites the other person, who onboards separately as a participant ([UC-ONB-12]). Participant capabilities are not a separate module — they are part of the shared-account module.

Database alignment: the holder is represented by account_holder and owns the account; a participant is a separate account_participant row linked to the holder, with role capabilities, account scopes, and grant/revoke overrides. The holder is not automatically modelled as a participant. Participant lifecycle therefore needs its own use cases: invitation, activation, rights change, card issuance, card limits, statement visibility, revocation, participant death/incapacity, and holder notification ([UC-SUB-26]).

UC-SUB-14 — Charge failure (no funds)

The internal transfer for the fee fails because the account has no money (or only part of it — we then take a partial payment, [UC-BIL-24]). The account stays open and the subscription stays active (not closed); the fee invoice remains open / partially-paid → overdue (still owed, [UC-BIL-06]). An unpaid fee does NOT trigger AML/fraud suspension ([UC-SUB-22]), but it may trigger a commercial arrears state: dunning, retry, collection flags, and restriction of optional paid actions (for example paid card reissue or paid upgrade) until the arrears are settled. Spending is not assumed impossible: offline card presentments, delayed fees, and scheme liabilities can create negative positions ([UC-SUB-24]).

Dunning / retry cadence (quasi-exponential, relative to the due date): -1 day (pre-notice), +1 day, +1 week, +1 month, +2 months; then stop — no further retries. The invoice remains overdue/owed and is eligible for write-off ([UC-BIL-10]).

Accepted structural risk: because fees are collected only from the GG balance ([UC-BIL-05]) and the account is never pushed negative for our own fee, a customer who keeps the account near-empty is slow to collect from. We mitigate by partial collection with no minimum and by seizing incoming funds toward the debt ([UC-BIL-24]); a customer who games this by emptying before every cycle stays in debt, and after persistent failure is written off and the account closed. The residual exposure is accepted: nearly all perks (cashback, card-payment insurance, even the card scheme cost) are payment-linked, so an unused account costs Green-Got little.

UC-SUB-15 — Savings module subscription (free, intermediated)

A livret/ASV/PER subscription provisions/links an investment contract; it is free to the customer (no billing line); Green-Got revenue is partner commission ([UC-ACC-07]). subscriptions references the investment contract; it does not re-model it.

Product-specific distributor/partner constraints are reserved in [UC-REG-06].

UC-SUB-16 — Card as a sub-module

A card is a sub-module of an account module. Product/tier lives per card (matching CardProductTier), not on the account — so different cards on the same account can be different products. Supported configurations:

Adding/removing/retiering a card is a module-level change within the subscription. Normally a card is linked to an account, which is linked to a subscription. Exception: cards can be pre-created, linked to nothing yet, and bound to an account later via the event-onboarding flow ([UC-ONB-19]).

UC-SUB-17 — Grandfathered subscription after offer retirement

A subscription on a retired offer continues unchanged; it migrates only on an explicit event: dismantle, upgrade/downgrade, accepted offer change, or a licence migration ([UC-MIG-03]).

Forced-break exception: we must also be able to break a grandfathered subscription when we have to — e.g. the product becomes illegal / non-compliant, or there is a genuine material configuration error that cannot be honoured safely. Legal/compliance breaks may be immediate where required; commercial/material-error breaks that raise price or reduce rights must go through [UC-REG-01] notice/rejection rules and are not a silent unilateral migration. This is the escape hatch from “honour forever”: admin/compliance-initiated, audited, and reserved for legal or material error reasons. Governance: a forced break is a C-level decision — the C-suite is involved in offer decisions, so breaking grandfathered customers escalates to them; it is audited. Criteria are legal/compliance or genuine material error (above); notice and consumer-protection on any price rise follow the framework-change rules ([UC-REG-01]).

UC-SUB-18 — Activation provisions accounts

On activation the concrete instances are created (retail → person + payment account + card; B2B → organisation + business account), by the onboarding orchestrator ([UC-ONB-13]); the generic crates only record the resulting links.

UC-SUB-19 — Voluntary subscription closure (request → async wind-down)

A customer cannot truly close their account instantly. They request closure; we collect payout / transfer instructions for any remaining funds, show the commercial effective date (normally the end of the current paid period for a monthly commitment), and put the account into closing at that effective date: no new activity (incoming SEPA credits bounce / are returned to sender, not accepted), but ongoing transactions, disputes, scheme presentments, legal holds, and investigations must settle before the account can be archived. A non-empty account can still enter the closure flow; the funds are paid out during wind-down once legally and operationally available. Archival is blocked until all ongoing transactions, disputes, investigations ([UC-SUB-22]), legal seizures ([UC-REG-05]), and negative positions ([UC-SUB-24]) settle. Legally mandatory immediate termination/pro-rata correction paths are handled by [UC-REG-01]/[UC-BIL-09].

Firm decision — incoming credits after closure bounce; we do not redirect or manage them. Once closure is requested, incoming SEPA credits (salary, etc.) are returned to sender — exactly as a sold car can no longer be driven. We do not run forwarding/redirection (costly, slow, and we don’t even know the customer’s next IBAN). The customer is not stranded: they have another account elsewhere and are responsible for telling their employer/payers; the receiving bank handles utility re-pointing via standard mobility. This is deliberate and accepted.

UC-SUB-20 — Age-out (continuing-eligibility) transition

A holder on a youth offer reaches the boundary (18th birthday). The transition takes one of two forms, depending on whether the fallback can be applied with data we already hold:

UC-SUB-21 — Bundle merge / cheaper-offer suggestion (the reverse of explosion)

When a holder ends up with separate unit offers that together qualify for a bundle or a cheaper offer, we move them onto the cheaper combination so they pay less — but deliberately not during onboarding. We do it later, as a “good news” moment that shows we care: the customer onboards for the new product as normal; once that onboarding is validated (by compliance or by the server), we wait ~1 day, then automatically migrate them to the bundle at the next period boundary (close the unit subscriptions, open the bundle — close-then-open + filiation) and email them that we’ve moved them to the bundle and everything is now cheaper. Boundary timing avoids mid-period refund mechanics. Where it can’t be safe-automatic, route to customer support. Respects collision/eligibility ([UC-SUB-12]); inverse of [UC-SUB-05]. Best bundle = the cheapest applicable one. If a merge would require customer consent, we do not auto-apply it — instead we alert customer support to handle it ([UC-BO-08]) (this should be rare, since a price decrease normally needs none).

UC-SUB-22 — Suspension (AML / fraud / security investigation)

Suspension is a real subscription state, used only for AML / fraud / security reasons — never for an unpaid fee ([UC-SUB-14]). The BO can suspend an account while it is under investigation. While suspended:

Suspension is never an account closure; it is lifted (or escalated to closure) by the outcome of the investigation.

Authorisation is asymmetric: anyone can suspend, with no delay — customer support who spots something weird suspends and forwards the case to a compliance officer; a compliance officer suspends directly; or an automated rule suspends when specific scenarios are met. Lifting a suspension is privileged — restricted to managers or compliance officers.

Notification is mandatory with the reason by default (PSD2 art. 68 / CMF): the customer is informed, before the block or immediately after, and told why. No-tipping-off is a narrow exception, applying only when the block is linked to an AML suspicion (déclaration de soupçon to Tracfin, or an asset-freeze measure, LCB-FT / CMF L.561-x) — there we notify that the account is restricted without the reason. The branch is driven by a classified reason code, not operator discretion: fraud / security / risk-on-instrument (incl. “support spotted something weird”) → notify with reason; AML-suspicion-linked → notify without reason. Support cannot select the silent branch; it requires the suspicion/Tracfin pathway to be engaged.

Blocking funds movement and blocking account closure are lawful only in the AML-classified branch, on its own legal basis. An operational (art. 68) freeze blocks payment means but does not suspend the customer’s right to terminate the framework contract ([UC-SUB-23]).

Proportionality: a support-initiated freeze has a max duration before it must be lifted or escalated to a named owner (fraud / compliance / AML officer) who confirms it, reclassifies it as a formal AML measure, or releases it — it cannot sit indefinitely in “support spotted something” state. ⚠️ OPEN: the max-duration value; how billing behaves during suspension.

UC-SUB-23 — Commercial cancellation at period boundary

Self-service cancellation is allowed without friction. The payment-account framework contract is always terminable at will ([UC-SUB-09]); only the commercial effect timing differs by cadence:

This is the commercial default UX. The customer always retains the legal at-will framework-contract termination with pro-rata prepaid-fee refund ([UC-SUB-30], CMF L314-13), which overrides the monthly “no unused-time refund” line above when invoked. At the boundary the subscription ends and account closure/wind-down follows [UC-SUB-19] if the account is being closed. A pending cancellation can be revoked before the boundary (the subscription is still active until then); re-subscribing after the boundary on a still-open account is a normal subscribe / offer change ([UC-ONB-18]/[UC-SUB-06]) — no special reactivation path is needed.

UC-SUB-24 — Negative balance / offline card presentment

Two distinct objects, not to be conflated:

Only transactions with a valid scheme dispute/chargeback reason are disputed; “no funds” alone is not a dispute reason. Unrecovered amounts go through dunning, collections/write-off ([UC-BIL-10]), and risk monitoring. ⚠️ OPEN: the cap value and the recovery-window N.

UC-SUB-25 — Dormant / inactive account

An account with no balance and no customer activity is not automatically closed just because it is commercially inactive. It is flagged for AML/fraud monitoring, customer reachability checks, and the legally required dormant-account process (loi Eckert, [UC-REG-12]). Dormancy does not erase access to statements or regulatory records ([UC-REG-05]). A long-idle positive balance is also a PI-perimeter concern ([UC-REG-04]), not only an Eckert one.

UC-SUB-26 — Participant lifecycle and rights

A participant is invited by the holder, completes their own onboarding, receives rights through a role plus optional grant/revoke overrides, and may have account scopes restricting which holder accounts they can access. Supported lifecycle events: invite expiry, activation, rights change, card issuance/reissue, card limit change, statement/export visibility, suspension, revocation, participant death/incapacity, and holder notification. A participant can view/act only within their configured rights; they never own the account or the money. A minor-participant ([UC-ONB-11]) is a constrained variant: access only via the dedicated child app, under enforced age limits; those constraints lift at majority ([UC-SUB-20]).

UC-SUB-27 — Primary holder death (manual termination)

On notification of the primary holder’s death we stop accruing subscription fees immediately and freeze the account ([UC-REG-05]) — we never keep billing a deceased customer’s estate. (We are aware of the 2025 cap on bank succession feesfrais bancaires de succession, free under the legal thresholds; our “never bill the estate” stance is deliberately more conservative and safely compliant.) Handling is then manual: we act on the notary’s communication, which instructs us on the process (funds, payout, closure). This is a special case of manual termination ([UC-BO-08]), audited and reason-coded. The ASV/life beneficiary-clause payout is legally distinct from bank-account succession and is owned by the insurer/partner ([UC-REG-06]), not by customer_billing.

UC-SUB-28 — Card lifecycle (renewal, reissue, tier change)

A card is a sub-module of an account ([UC-SUB-16]); its lifecycle events:

UC-SUB-29 — Customer-specific subscription modifiers

Beyond the offer’s intrinsic price ([UC-OFF-03]), a subscription carries customer-specific modifiers, read at each billing cycle and applied at the invoicing layer — so two holders of the same offer can be billed differently:

These modifiers live on the subscription/customer, never on the immutable offer — keeping offers fixed-price ([UC-OFF-06]) while still allowing per-customer pricing.

UC-SUB-30 — Framework-contract termination (legal, at-will)

Distinct from the commercial boundary cancellation ([UC-SUB-23]): the customer may terminate the payment-account framework contract at any time (CMF L314-13). Model:

This is the path a customer invokes to leave with money back; boundary cancellation ([UC-SUB-23]) stays the default no-action UX. Account wind-down follows [UC-SUB-19].

UC-SUB-31 — Period, billing cycle & boundary (definitions)

The load-bearing time vocabulary, defined once. A subscription has a billing cadence (monthly or annual). Its period is the span the current charge covers; the anniversary is the day the period renews — the subscription’s own anchor ([UC-BIL-02]); a period boundary is the instant one period ends and the next begins. Behaviours keyed to the boundary: continuous-eligibility re-check ([UC-OFF-04]), downgrade effect ([UC-SUB-07]), commercial cancellation ([UC-SUB-23]), bundle-merge ([UC-SUB-21]), modifier read ([UC-SUB-29]). An upgrade resets the anchor to the switch date (close-then- open, [UC-BIL-08]) — so a subscription’s anniversary can drift. One-off fees are point-in-time and not tied to the period ([UC-BIL-04]).

UC-SUB-32 — Subscription snapshot & filiation (immutability contract)

A subscription freezes what it references at creation, so later catalogue/price changes never mutate a live subscription:

Customer-specific modifiers ([UC-SUB-29]) are applied at the invoicing layer on top of the snapshot, never by mutating it. A change is always close-then-open ([UC-SUB-12]): the old subscription is expired and a new one created, linked by filiation (previous_subscription_id) for audit/lineage. This is what makes grandfathering ([UC-SUB-17]) and clean migration ([UC-MIG-03]) work.

UC-SUB-33 — Three distinct lifecycles (offer / subscription / account)

Three separate state machines that interact but never collapse into one:

Consequences: a subscription ending does not instantly close the account; a retired offer does not close live subscriptions ([UC-SUB-17]); suspension acts on the account, not the subscription’s commercial state.


5. Customer billing (customer_billing)

UC-BIL-01 — First invoice on activation (always billed in advance)

Principle: a recurring subscription fee is always billed in advance — at the start of the period it covers — never in arrears. A paid offer issues its first invoice when the subscription period opens (the obligation arises), independent of whether/when the charge clears. The customer pays upfront for the upcoming period; accounting then recognises it over that period (deferred revenue, [UC-ACC-01]). (One-off fees are different — billed at the event, [UC-BIL-04]; “billed in arrears” [UC-ACC-04] is an accounting/accrual case, not how subscriptions are charged.)

UC-BIL-02 — Recurring billing at the anniversary (in advance)

Each cycle bills in advance at the subscription’s anniversary date (or the earliest possible date before it) — never at period end; per-subscription anchor. A bundle bills as one charge for the offer.

UC-BIL-03 — Gross + discount lines

An offer with a promo bills a gross price line + a discount line, net = charged — preserving the gross-revenue vs discount distinction for accounting/reconciliation.

UC-BIL-04 — On-the-fly fee (ATM, FX, reissue)

A chargeable transaction creates a BillableEvent captured at the moment it happens, recognised at the event date. Decision: per-event line granularity — each fee is one invoice line with its exact recognition date; we do not collapse a day’s fees into a single batched line. These per-event lines sit on the customer’s invoice and are aggregated for the customer on the monthly statement ([UC-BIL-11]).

UC-BIL-05 — Charge by internal transfer (no SEPA DD)

The fee is collected by an internal transfer from the customer’s payment-account sub-ledger (represented inside the safeguarded/ring-fenced Crédit Mutuel liquidity pocket) to Green-Got’s functioning/operating account at Crédit Mutuel — no SEPA direct debit. Payment settles a receivable; it never creates revenue. The accounting model must preserve the hard boundary in [UC-REG-04]: payment accounts are customer positions over safeguarded funds, not Green-Got deposits or savings accounts.

UC-BIL-06 — Failed/declined charge → unpaid invoice

A failed internal transfer (insufficient funds) produces an unpaid invoice (open → overdue) — or a partially_paid one where only part was available ([UC-BIL-24]) — not the absence of an invoice. Drives the dunning/retry cadence ([UC-SUB-14]).

UC-BIL-07 — Commercial gesture (e.g. app-down free month)

An ad-hoc gesture that reduces a customer invoice is a credit note with a reason code against the issued invoice (or applied to the next), keeping gross revenue, reduction, and audit trail distinct. By default this is a reduction of revenue (contra-revenue), not a marketing expense. Distinct from an offer-intrinsic promo ([UC-OFF-03]). If the gesture is not linked to a customer invoice (cashback, bonus, cash reward, pure compensation), it is not modelled as an invoice credit note; it follows the reason-code treatment in [UC-BIL-16]/[UC-ACC-06].

UC-BIL-08 — Proration close-then-open with netting (upgrade)

At an upgrade switch: stop the old sub and open the new one with its own anniversary at the switch date. The invoice carries two visible lines — (1) the full price of the new subscription for its first period, and (2) a prorata credit note for the old subscription’s unused prepaid days (daily pro-rata on gross) — and nets them so the customer pays the difference. Each line carries its own recognition period; the filiation link is recorded. This supersedes the billing draft’s “prorated new invoice” example: the new line is full price; the proration lives in the credit line. Consequence (confirmed): the billing anchor moves to the switch date — the anniversary intentionally drifts with each upgrade. (Downgrades produce no credit note — perks kept to period end, [UC-SUB-07].)

UC-BIL-09 — Refunds and payouts: commercial “no”, legal/correction “yes”

The commercial product posture is no voluntary early unused-time refund: downgrades and cancellations are normally effective at the period boundary, with perks retained until then ([UC-SUB-07], [UC-SUB-23]). That is not an absolute accounting rule. The model keeps an auditable refund/payout mechanism for legally required, correction, and residual cases:

Not a refund: returning a customer’s own balance on account closure is a payout of their own funds ([UC-SUB-19]), not a refund — it does not use the refund machinery.

UC-BIL-10 — Write-off vs credit note

An uncollectible valid debt is written off (revenue kept, loss recognised), and dropped from active receivables — not credit-noted. A credit note is for when the customer no longer owes (error/dispute/gesture). A written-off invoice that is later paid → recovered.

UC-BIL-11 — Monthly statement

A statement (non-fiscal) aggregates a customer’s period: opening balance, invoices issued, payments, credit notes, closing balance. One statement can show several invoices.

UC-BIL-12 — Invoice for everything, gap-free numbering

Every charge has an invoice (subscription fees and one-off fees alike), numbered sequential and gap-free per legal entity. Because the issuer is Green-Got itself, this is Green-Got’s own gap-free series — distinct from the invoicing crate, whose numbering belongs to the business customer issuing to their clients (a different issuer). Same gap-free discipline (FR art. 289 CGI), separate sequence.

UC-BIL-13 — Free offer → no recurring invoice (but fees still apply)

A 0-priced offer produces no recurring subscription invoice (no recurring obligation arises). One-off fees still bill normally — a lost-card replacement or ATM withdrawal creates a BillableEvent and an invoice line exactly as on a paid offer ([UC-BIL-04], [UC-OFF-08]). A €0 offer produces no invoice at all — not even a €0 invoice.

UC-BIL-14 — Rétractation / cooling-off (non-waivable refund path)

Onboarding is entirely at distance, so withdrawal/renunciation is not an edge case — it overlaps the first paid period for essentially every paid signup (the first fee is collected on activation, [UC-J-01]), and must be supported from v0:

Express consent to begin performance. Because the first fee is collected on activation inside the 14-day window, onboarding must capture the customer’s explicit consent to start the service during the cooling-off period (C. consommation). With that recorded consent, a later withdrawal reimburses the unused portion but the customer owes for service actually rendered up to withdrawal; without it, the early charge is exposed and fully refundable. The consent is captured and stored as part of activation. (NB on the Steam comparison: digital content can fully waive withdrawal once immediate delivery is consented; distance financial services have a separate, non-waivable 14-day regime — you cannot extinguish it, only owe for service rendered — which is why a bank can’t “refuse” it the way a games store can.)

⚠️ OPEN: exact per-product window and full-vs-pro-rata regime, owned with legal.

UC-BIL-15 — Savings: no customer invoice

Savings/ASV/PER bill the customer nothing; the only economic event is partner commission, which lives in accounting only ([UC-ACC-07]).

UC-BIL-16 — Reason-coded credit / refund / reward classification

Every credit note, customer credit, refund liability, cashback, bonus, and exceptional compensation must carry a reason code. The reason code drives accounting classification and audit:

Reason codes are part of the billing source data exported to accounting and the audit trail.

UC-BIL-17 — Free-month credit (counter on the subscription)

A free-month counter lives on the subscription. An operator grants N free months ([UC-BO-05], audited + reason-coded). At each billing cycle the counter is interrogated: if > 0, the cycle invoice is issued with the normal gross line + a 100% free-month credit line (reason-coded gesture, net €0, contra-revenue, [UC-BIL-07]/[UC-BIL-16]) and the counter is decremented by one; at 0 the cycle bills normally. There are no free trials in the product; this same mechanism would back any future free-first-month promotion.

UC-BIL-18 — E-invoicing & e-reporting (FR reform)

Every invoice Green-Got issues is built e-invoicing-compatible (structured format, Factur-X / PDP-ready). The reform has two distinct limbs:

Reception for all from Sept 2026; émission / e-reporting phased Sept 2026 → 2027. (Green-Got is the issuer — distinct from the invoicing crate, [UC-BIL-12].)

UC-BIL-19 — Promo-code redemption

A customer redeems a code (e.g. SUMMER20); the system validates it (validity window, per-customer single-use, eligibility) and attaches the resulting discount (gross + discount line, [UC-BIL-03]) or free months ([UC-BIL-17]) to the subscription’s modifiers ([UC-SUB-29]), applied at the invoicing layer. Promo codes are customer-specific, not offer-intrinsic ([UC-OFF-03] unchanged). ⚠️ OPEN: anti-abuse (multi-redemption, self-referral) and stacking rules between codes.

UC-BIL-20 — Referral / parrainage credit

When a referred prospect subscribes and meets the qualifying condition (e.g. account funded/active), the referrer’s subscription free-month counter is incremented ([UC-BIL-17]/[UC-SUB-29]), reason-coded ([UC-BIL-16]); the referee may receive a symmetric credit. Fraud controls: qualifying event before credit, self-referral dedupe, and per-referrer caps. Referral rewards are regulated acquisition premiums ([UC-REG-10]). ⚠️ OPEN: qualifying condition, credit amounts, and caps.

UC-BIL-21 — Online withdrawal / renunciation function

Distance financial contracts concluded through an online interface must offer an online withdrawal function (Directive (EU) 2023/2673, applicable 19 June 2026): the customer exercises rétractation / renonciation directly in-app/web, not only by letter. Requirements:

Triggers the refund path ([UC-BIL-14]/[UC-BIL-09]); for savings, commission clawback ([UC-ACC-15]).

UC-BIL-22 — Cashback / spending rewards

A reward earned on card spend, credited to the customer as a customer-credit / pending-balance liability (not an invoice credit note), reason-coded ([UC-BIL-16], [UC-ACC-06]). Funding sources are explicit, not generic “interchange”:

It is distinct from an acquisition prime ([UC-REG-10], conditioned on signup/referral) and a commercial gesture ([UC-BIL-07], invoice reduction for a service issue). Controls: eligibility per spend, caps, fraud/abuse checks, tax/social treatment. ⚠️ OPEN: reward rates and tax classification (funding source resolved above).

UC-BIL-23 — Money conventions

All amounts are in EUR, stored as integer minor units (cents) — never floats. Lines distinguish gross / discount / net and VAT ([UC-BIL-03], [UC-ACC-08]); the customer-facing charge is the net, gross-of-VAT amount. Rounding is per line at issuance and snapshotted ([UC-SUB-32]). Multi-currency is out of scope (EUR only) for now. (Detailed rounding/precision rules belong to the customer_billing crate doc; this fixes the conventions the use cases assume.)

UC-BIL-24 — Partial payment & carried shortfall

If the balance can’t cover the full fee, we take a partial payment rather than nothing: collection transfers what is available (e.g. €5 of a €10 fee) and allocates it to the invoice ([UC-BIL-05]). The invoice goes open → partially_paid with a remaining balance (€5 still owed); it is not mutated (invoices are immutable, [UC-BIL-12]).

The shortfall is carried forward: at the next cycle the new period’s invoice is issued as normal (€10), so the customer now owes the new invoice + the still-open shortfall (€15 total). Collection attempts cover open balances oldest-first, and the monthly statement ([UC-BIL-11]) shows the combined amount due (€15) as one figure — even though it spans two immutable invoices (we do not roll the old shortfall into the new invoice). Each open balance follows the dunning/retry cadence ([UC-SUB-14]); persistently uncollected balances are written off ([UC-BIL-10]).

No minimum threshold — we take whatever is available, however small (owe €10, have €0.10 → take the €0.10; next period, invoice again and take whatever is there). Anti-gaming: a customer who empties the account before each cycle to dodge the fee stays in debt; incoming funds are swept to the outstanding balance first (we never push the account negative for our own fee, [UC-SUB-14]) — i.e. when money appears we seize the amount due before it can be spent — and persistent avoidance ends in account closure ([UC-SUB-19]) after write-off ([UC-BIL-10]).


6. Accounting (accounting)

UC-ACC-01 — Subscription recognised over the service period

Fee billed in advance: invoice on day 1, revenue earned over the period; the unearned portion sits in deferred revenue (produits constatés d’avance) and releases as the period elapses. Annual prepaid subscriptions therefore create PCA and release prorata temporis across the service period.

UC-ACC-02 — Annual prepaid → deferred revenue

Annual fee billed in advance: spread across 12 periods; ~11/12 deferred at the end of month 1.

UC-ACC-03 — One-off fee recognised at event date

ATM/FX/reissue fee: recognition_period_start = end = event date; recognised immediately, even if cash clears in a later period.

UC-ACC-04 — Billed in arrears → accrued income

Service rendered before invoicing → accrued income (facture à établir); line-level recognition period drives the correct month.

UC-ACC-05 — Discounts / promos as contra-revenue

Offer-intrinsic discounts and invoice-reducing commercial gestures reduce revenue (gross line + discount/credit contra-line). They are not marketing expenses unless they remunerate a distinct service, which is outside the current commercial-billing model.

UC-ACC-06 — Commercial gesture treatment

Treatment is reason-code driven: each reason code maps to contra-revenue (credit note) / customer-credit liability / expense per the canonical taxonomy in [UC-BIL-16]. The reason code is mandatory — it determines the GL treatment (revenue vs liability vs expense) and supports audit.

UC-ACC-07 — Partner-commission revenue (savings)

Savings/ASV/PER/livrets distributed through partners generate commission paid by the partner, with no customer invoice. This is a distinct revenue line from subscription fees and exists outside customer_billing. Commission may be upfront at subscription and/or recurring on outstanding assets, depending on the partner contract/product. It is recognised when earned from the partner commission event/statement/accrual basis configured for that product. Upfront commission is exposed to clawback (reprise) on renunciation/early surrender: recognise net of an expected-clawback provision ([UC-ACC-16]) and fire the symmetric reversal event on exit ([UC-ACC-15]); clawback terms are partner reference data ([UC-ACC-14]).

UC-ACC-08 — VAT treatment split

VAT is tax-code driven, not hard-coded by the catalogue. Finance baseline:

Each product/fee carries a dated tax_code whose effective-dated policy can change (for example if Green-Got renounces the 260 B option). VAT is resolved at issuance from the product/fee tax_code + dated policy/rate table + territory, then snapshotted on the line.

UC-ACC-09 — Credit note reverses revenue + recovers VAT

A credit note reverses the line’s revenue and the associated collected VAT.

UC-ACC-10 — Write-off → loss, revenue kept

A write-off ([UC-BIL-10]) keeps the original revenue and recognises a loss (créance irrécouvrable); not a revenue reversal.

UC-ACC-11 — Receivables ↔ GL reconciliation

At any date, Σ open invoice balances = GL client-receivable balance; supports aged-balance buckets and doubtful-debt provisioning, and feeds RUBA/ACPR inputs.

UC-ACC-12 — Period close / cut-off

Month/year close shows correct rattachement from line-level recognition periods, with deferred revenue and accrued income already positioned — no manual cut-off for these lines.

UC-ACC-13 — Billing source data vs GL mapping

The commercial/billing domain produces accounting-source data: gross revenue, discounts/credit notes, VAT/tax code and rate, recognition period, receivable/payment state, refund/customer-credit liability, reason code, product/module reference, partner commission events, and customer/legal-entity references. It does not own the chart-of-accounts mapping or double-entry journal generation. GL account mapping and journal depth live in a separate accounting integration layer fed by these source fields.

UC-ACC-14 — Partner commission & clawback schedule (reference data)

Per partner × product × commission-type, holds the terms downstream reads from — nothing about clawback %/windows is hardcoded: renunciation_window_days; ordered, contiguous, non-overlapping reprise_periods[] of {from_month, to_month, clawback_pct} (final tier 0% = the point past which commission is unconditionally earned); clawback_basis; settlement_mode (netting default | invoice); and a PER per_transfer_triggers_clawback flag.

UC-ACC-15 — Commission clawback / reversal on renunciation or early surrender

Symmetric negative event to [UC-ACC-07]; must exist whatever the recognition policy, because the insurer’s netting has to land in the ledger. Triggers: policy_renounced (within the window → 100% reversal); policy_surrendered_early, and PER transfer_out where flagged (→ reverse the tier clawback_pct for the exit month per [UC-ACC-14]). Posts contra-revenue, or draws down the provision if [UC-ACC-16] is live. The event carries policy_ref + originating_commission_booking_id; with netting, period events sum to the partner statement’s negative line (residual = settlement variance).

UC-ACC-16 — Expected-clawback provision at booking

Recognise full upfront commission and simultaneously raise a clawback_provision liability = expected_clawback_rate × commission (rate per partner × product from observed renunciation/surrender curves, versioned). Actual exits draw down the provision rather than taking a fresh revenue hit. This is the IFRS 15 variable-consideration constraint / French-GAAP prudence applied — recognise only to the extent it is highly probable it will not reverse.

UC-ACC-17 — Periodic clawback-provision true-up

Monthly, aligned to the partner settlement cycle. Per partner × product cohort: compare cumulative actual clawbacks ([UC-ACC-15]) to the provision held, re-estimate the rate from trailing actuals, and adjust the provision — increase → contra-revenue; release → revenue. When a policy passes the final tier (0%), its remaining provision is released: the commission is now unconditionally earned.


7. Migration & transitions (offers + subscriptions + customer_billing + accounting)

UC-MIG-01 — Add a module to an existing customer (commercial view)

The commercial counterpart of [UC-J-03]/[UC-ONB-10]: an existing holder takes an additional offer; a new subscription is created; dependencies and collisions are enforced.

UC-MIG-02 — Product migration (offer change)

Upgrade/lateral/downgrade as offer changes ([UC-SUB-06]/[UC-SUB-08]/[UC-SUB-07]); always close-then-open with filiation and the billing/accounting consequences above.

UC-MIG-03 — Agent-PI → independent-PI customer-base migration (the big one)

Sub-cases (each gets rules + tests):

UC-MIG-04 — Catalogue migration (legacy hard-coded plans → offer rows)

Today’s hard-coded plans (no catalog, no versioning) are reified as offer + module rows in the new catalogue. This is the data prerequisite for [UC-MIG-03].

UC-MIG-05 — Offer retirement & grandfathering

Generalised retirement ([UC-OFF-05]/[UC-SUB-17]): a retired offer keeps its live subscriptions honoured until an explicit migration event.


8. Back-office & dead-ends (* — all crates)

Principle: the BO can do everything self-service can — and recover every dead-end, whatever the cause (including the customer simply can’t log in). Every BO action is audited with a reason code.

UC-BO-01 — Act on behalf of a customer

A support/compliance officer performs any self-service flow (subscribe, change, cancel) for a customer who cannot do it themselves. Phasing: the full act-on-behalf surface is eventual, not v0. But a minimum set of BO/manual paths must exist from v0, because they back non-waivable rights and dead-end recovery: cancellation/termination ([UC-SUB-23]/[UC-SUB-30]), withdrawal/rétractation ([UC-BIL-14]/ [UC-BIL-21]), complaints ([UC-REG-11]), account closure ([UC-SUB-19]), and access recovery (the customer simply can’t log in, [UC-BO-08]).

UC-BO-02 — Bundle explosion in the BO

Support performs the dismantle/explosion ([UC-SUB-05]) when self-service can’t (e.g. incompatible replacement).

UC-BO-03 — Offer change in the BO

Support performs an upgrade/lateral/downgrade, including bundle changes that require exploding into unit offers.

UC-BO-04 — Apply a commercial gesture

Support issues a gesture credit note with a reason code ([UC-BIL-07]).

UC-BO-05 — Comp / free month

Support grants one or more free months by incrementing the subscription’s free-month counter ([UC-BIL-17]) with a reason code — consumed automatically at each billing cycle; or assigns a free version where appropriate.

UC-BO-06 — Remediation injection (onboarding)

Officer injects a missing data point to re-open a specific Step ([UC-ONB-06]).

UC-BO-07 — Reset / restart an onboarding

Officer performs a partial or full reset of an onboarding session. The full reset is an archive: the officer flips an in-flight session to the terminal Abandoned state (audited, reason-coded [UC-BO-10]), which frees the one-in-flight slot ([UC-ONB-03]). This is the escape hatch when a prospect picked the wrong offer / legal form and the resume redirect would otherwise trap them: after archival the customer’s app finds no in-flight onboarding ([UC-ONB-03]), clears its persisted onboarding id, and restarts from the offer gate ([UC-ONB-01]). Mirrors the automatic archival of abandoned/retired sessions ([UC-ONB-15]/[UC-ONB-23]).

UC-BO-08 — Manual dead-end recovery (generic escape hatch)

A very manual process to fix any state the automated flows can’t reach (broken bundle with no clean fallback, partial provisioning, migration straggler), with full audit + reason codes. It is deliberately generic — its purpose is to handle the unenumerable, so there is no need to pre-catalogue every dead-end. Recurring dead-ends that prove common graduate into proper automated / canonical flows over time.

UC-BO-09 — Eligibility / collision override

An officer can override commercial eligibility/collision rules by force-creating or force-changing a subscription with justification. Two controls always apply: everything is fully auditable ([UC-BO-10]), and any manual modification requires manager approval at final validation (maker-checker / four-eyes). This generalises to BO actions overall: a manual change is only committed once a manager has approved it.

The BO cannot override hard regulatory / legal constraints, including:

If a hard constraint blocks the desired outcome, the BO can only route to the appropriate legal, compliance, or partner process; it cannot force the subscription into existence.

UC-BO-10 — Audit & reason codes

Every BO action writes an audit event with actor, timestamp, reason code, and before/after — the foundation for compliance review.

UC-BO-11 — Manage the restricted-offer whitelist

A customer-care officer adds (or removes) an email/phone on the per-offer whitelist that unlocks a restricted offer for that identity ([UC-OFF-12]) — e.g. granting an ambassador/employee access to a price-0 offer ([UC-ONB-09]), or an invitee access to a closed-beta offer. The whitelisted identity then sees and can subscribe to that offer at the subscribable-offers gate ([UC-ONB-01]/[UC-ONB-18]). Audited with a reason code ([UC-BO-10]).

UC-BO-12 — Onboarding risk & signals panel

The back-office onboarding detail page ([UC-BO-06]) carries a risk & signals panel that gives a compliance/fraud officer everything needed to judge an onboarding, built from the event log ([UC-ONB-35]) and the PEP declaration ([UC-ONB-36]). It has four sections:

The panel is read-only and audited on read where required ([UC-BO-10]); it informs but does not replace the officer’s validate/request-changes/reject decisions ([UC-ONB-05]/[UC-ONB-06]/[UC-ONB-07]).

UC-BO-13 — Segmented validation queues (business vs retail)

The officer validation queue ([UC-ONB-05]) is split by segment into two independent work queues — a business queue and a retail queue — so compliance/customer-care officers can triage the two customer populations separately (they have different documents, controls, and reviewers). Each queue is the same submitted/under-review list ([UC-BO-07]), filtered to the onboardings whose offer segment is Business or Retail respectively.

The segment is an axis of the offer ([UC-OFF-05]); offers are code-defined and not joinable in SQL, so it is denormalised onto the onboarding session at create (and is invariant across a confirm_offer sibling switch, since a variant group is single-segment — [UC-ONB-34]). This lets each queue filter and paginate by segment at the database level. See Onboarding · 7. Data Model. The back office groups both queues, plus identity verifications and the full onboarding list, under a single Validation navigation group.


9. Non-waivable regulatory / payment-service rights (REG)

These use cases are not the commercial domain’s full operational implementation. They are the hard boundary conditions the commercial model must respect and expose hooks for. A commercial rule can be stricter or more conservative, but it cannot remove these paths.

Country scope. Green-Got operates in France only today, so every rule in this section is France-specific and applies only to French-resident accounts. The catalogue is country-aware ([UC-ONB-01]); France-only behaviour must be gated on country = FR so it neither executes nor is explored for non-FR accounts. When Green-Got expands, each country gets its own regulatory variant ([UC-REG-15]).

UC-REG-01 — Framework contract lifecycle, durable medium, and unilateral changes

Payment-account/card subscriptions sit under a framework-contract lifecycle. The commercial catalogue must support:

Any forced migration, bundle explosion, retirement fallback, or material price/perk change that raises price or reduces rights must reference this use case. If product wants “monthly engagement ends at the end of the month”, model that as the commercial default; do not delete the legal override path.

UC-REG-02 — Ongoing compliance gates

Compliance gates are not commercial eligibility. They are continuously or periodically checked while the relationship exists:

No-tipping-off constraints override ordinary customer notification wording. Commercial subscriptions must tolerate compliance freezes, remediation steps, and forced closures without treating them as voluntary cancellations.

UC-REG-03 — Payment disputes, unauthorised transactions, and chargebacks

Payment disputes are mostly owned outside the commercial domain, but the spine needs the hooks: unauthorised transactions, lost/stolen card claims, non-executed or defective transfers, card chargebacks, offline presentments, and evidence/burden-of-proof workflows. The commercial domain must record any resulting invoice credit, refund, receivable, write-off, or account adjustment, and must not confuse payment-service statutory restoration with a discretionary commercial refund.

UC-REG-04 — Payment-institution funds boundary

Green-Got manages customer payment accounts as sub-ledger positions over safeguarded liquidity held at Crédit Mutuel in a ring-fenced/safeguarded account. Green-Got also has its own functioning/operating account at Crédit Mutuel. Commercial billing may move fees internally from a customer’s payment-account position to Green-Got’s operating account, but the model must preserve these boundaries:

UC-REG-05 — Operational account lifecycle hooks

The commercial domain must expose states/references for account operations that are not purely commercial subscriptions:

Billing during a freeze: a legal seizure / garnishment / regulatory freeze does not cancel the subscription — it continues to bill; if the frozen funds prevent collection, the fee goes to arrears ([UC-SUB-14]).

The operational owner may live in core banking, compliance, legal operations, or customer support, but the commercial domain must not strand billing/subscription state when these events happen.

UC-REG-06 — Investment / insurance distribution boundary

Savings, ASV, and PER offers are commercial wrappers around partner/distributor flows. The commercial spine must reserve hooks for distributor status, suitability/advice or non-advice path, pre-contractual documents, partner acceptance/rejection, withdrawal/renunciation, transfer/surrender, commission disclosure, and partner commission accounting ([UC-ACC-07]). These are out of customer_billing unless a customer-facing Green-Got fee exists.

Two distinct distribution regimes — not interchangeable (Green-Got holds both registrations):

They are different registrations and different conduct regimes; the spine must route each product to its correct regime and not treat “savings” as one bucket ([UC-SUB-15]).

UC-REG-07 — Fiscal reporting for savings products (IFU / 2561)

Livret/ASV/PER carry a customer-facing annual tax-reporting obligation (IFU, formulaire 2561, and the equivalent life/PER declarations). This is typically owned by the partner/insurer, but the commercial spine must reserve the hook: who produces the document, and where the customer accesses it ([UC-REG-06], [UC-REG-05] post-closure access). Out of customer_billing (no Green-Got customer fee).

UC-REG-08 — Data retention, anonymisation & legal hold

Retention is purpose-based, not “keep everything forever”:

⚠️ Known GDPR risk — deferred. Blanket “invoices/ledger forever with the named party” collides with GDPR storage-limitation; the legal minima are far shorter (commercial 10y art. L123-22 C.com, tax 6y LPF, AML 5y L561-12). “Forever, non-anonymisable, named” is an over-retention posture a DPA would challenge. A defined max-retention + party-anonymisation strategy that preserves financial integrity (anonymise the party, keep the amounts) must eventually replace “forever” — deferred for now; the 10-year horizon gives us time to design it. The Tracfin legal-hold “keep forever” branch stays defensible; the blanket “everything forever” does not.

UC-REG-09 — Safeguarding mechanics (CMF L522-17)

Received payment-service funds are never commingled with Green-Got’s own or other persons’ funds, and funds still held at the end of the next business day (D+1) are placed in the distinct safeguarded account at Crédit Mutuel (ring-fenced) or covered by an equivalent guarantee. The model distinguishes:

D+1 control: a daily sweep moves received-but-unswept funds to safeguarding and reconciles the safeguarded balance against aggregate customer liabilities. Shortfall remediation: where Green-Got fronts a customer credit ahead of acquirer settlement ([UC-ONB-20]), it safeguards the equivalent from its own funds so the safeguarded pool always ≥ customer liabilities, reconciling when the acquirer settles; any shortfall is topped up from own funds. The model also exposes hooks for suspense breaks (unmatched funds aged in suspense), acquirer settlement shortfalls, and insolvency/safeguarding-event tagging (so customer positions are identifiable for the safeguarding regime). Out of scope here: transaction-level ledger reconciliation and the transaction store themselves (owned elsewhere) — this UC covers the safeguarding/suspense controls only.

UC-REG-10 — Regulated acquisition premiums (primes) & bundled offers

Cash/in-kind incentives to acquire or reward customers — the €50 event credit ([UC-ONB-19]), referral rewards ([UC-BIL-20]), sign-up bonuses — are primes regulated under CMF L312-1-2 (ventes avec primes / ventes liées on payment services): a threshold applies, with disclosure and conditions. Each campaign needs explicit approval, threshold compliance, customer disclosure, anti-abuse (dedup, caps, funding source), and tax/accounting treatment (acquisition cost / marketing expense, possibly taxable income for the recipient; reason-coded, [UC-BIL-16]).

Distinction: a prime (conditioned on opening/subscribing/referring) is regulated marketing under L312-1-2 and is not a [UC-BIL-07] commercial gesture (which reduces an invoice for a service issue or compensates within an existing relationship). Gestures are contra-revenue/compensation; primes are acquisition incentives with their own regulatory, disclosure, and tax regime.

UC-REG-11 — Complaints & mediation

The commercial spine must carry a complaint reference and its lifecycle: registration, acknowledgement, and regulatory response deadlines (ACPR recommendation: ack ≤ 10 business days, response ≤ 2 months; then médiateur referral). Any fee refund or commercial gesture arising from a complaint is handled through the existing machinery ([UC-BIL-07]/[UC-BIL-09]) and kept separate from the complaint record itself — resolving a complaint is not, by itself, a billing event. Operational ownership may sit in customer support/compliance; the commercial domain must not strand the linked billing/subscription state.

UC-REG-12 — Dormant / inactive accounts (loi Eckert)

Beyond the commercial dormancy flag ([UC-SUB-25]), inactive accounts follow the loi Eckert regime:

⚠️ OPEN: exact inactivity periods per product (and the post-death timeline), and notification channels.

UC-REG-13 — Annual statement of fees (relevé annuel des frais)

As a payment-account provider, Green-Got must produce, once a year and free of charge, a standardised statement of all fees charged over the year (relevé annuel des frais perçus, CMF L314-7) on a durable medium. The commercial/billing data ([UC-BIL-11] statements, [UC-BIL-04] fees) is the source; this is a distinct mandatory document from the monthly statement — produced per account, in the standardised format/terminology ([UC-REG-14]). FR-only ([UC-REG-15]).

UC-REG-14 — Fee information document (DIT) & standardised terminology

Pre-contract, Green-Got must provide a Fee Information Document (document d’information tarifaire, DIT) and use the EU standardised fee terminology / glossaire (Payment Accounts Directive) so fees are comparable across providers. The offer/catalogue’s fee schedule must map to the standardised terms, and the DIT must be deliverable on a durable medium at the right point in the onboarding/offer flow ([UC-ONB-01], [UC-REG-01]). FR-only ([UC-REG-15]).

UC-REG-15 — Per-country regulatory variants

The catalogue is country-aware ([UC-ONB-01]); today Green-Got operates in France only, and every FR rule (all of §9 and any France-specific behaviour elsewhere) is gated to French-resident accounts. On expansion, each country gets its own regulatory variant: withdrawal/renunciation windows, the competent ADR/ombudsman body, the dormancy regime, applicable consumer law, disclosure language, and tax reporting all vary by country of residence. France-only use cases must be conditioned on country = FR so they neither execute nor are explored for non-FR accounts; new countries add variants rather than overloading the FR rules. ⚠️ OPEN: the variant model (per-rule country matrix vs per-country rule set) — defer until the second country is real.


Appendix A — Locked premises

(Recorded from the working sessions; the per-crate docs are the authoritative source once written.)

Appendix B — Open questions rolled up

Consolidated for the per-crate uncertainties.md registers: ONB-10 (per-data-point validity windows), ONB-19 (unclaimed-card TTL), ONB-25 (auto-vs-manual validation risk thresholds), ONB-27 (provisioning compensation/rollback policy), ONB-29 (post-promotion credential bootstrapping for a newly created user), ONB-30 (claim of an anonymous session — identity-match anti-abuse), ONB-32 (duplicate-identity message — account-enumeration trade-off), ONB-20/22 (top-up suspense/refund/compliance handling), ONB-11 (youth card limits & age thresholds), ONB-16 (re-onboard data reuse), REG-01 (framework-contract notice/rejection/pro-rata rules), SUB-05 (price-increase consent/notice on explosion), SUB-22 (max-duration value; billing during suspension), SUB-24 (negative-balance cap value & recovery window N), BIL-14 (per-product withdrawal window & full-vs-pro-rata regime), BIL-16/ACC-06 (final reason-code taxonomy and audit ownership), ACC-07 (product-by-product partner commission trigger/accrual basis), ACC-08 (260B-option tax-advisor validation; product/fee tax-code catalogue and effective-dated policy table), REG-06 (investment/insurance distribution hooks), BIL-19/BIL-20 (promo/referral anti-abuse, qualifying conditions & caps), REG-08 (10-year anonymisation trigger & scope), REG-10 (prime threshold/approval/tax), REG-12 (Eckert inactivity periods & channels), BIL-22 (cashback rates & tax classification), SUB-30 (notice-period value), and all MIG-03 sub-cases. GL account mapping and double-entry journal generation are intentionally out of this use-case spine and belong to a separate accounting integration.

Accounting

0. Documentation Index

Accounting — Documentation Index

The accounting crate owns the general-ledger (GL) view of Green-Got’s own revenue: revenue recognition over the service period, deferred revenue (PCA) and accrued income (FAE), reason-coded contra-revenue, partner-commission revenue with clawback provisions, VAT determination, write-offs, and the receivables↔GL reconciliation and period close. It is segment-neutral derive-and-read logic fed by the customer_billing subledger; it does not own the chart-of-accounts mapping or double-entry journal generation — those live in a separate accounting integration layer ([UC-ACC-13]). These documents are the source of truth for the accounting domain.

Not accounting_export. This crate is Green-Got accounting its own revenue. It is distinct from business_domain/accounting_export, which exports a B2B customer’s own books (FEC / Pennylane) for their accountant. Same vocabulary, no shared data. See 1. Accounting Overview §2.

UC ids (e.g. [UC-ACC-01], [UC-BIL-16]) trace to the cross-domain spine ../../docs/0_use_cases.md.

Overview & boundary

Model

Behaviour

Architecture

Other

1. Accounting Overview

Accounting Overview

This document defines the scope of the accounting crate — Green-Got’s general-ledger (GL) view of its own revenue — and the two boundaries that shape it: the contrast with accounting_export, and the [UC-ACC-13] split between the billing source data this crate consumes and the chart-of-accounts mapping / journal generated downstream.

UC ids (e.g. [UC-ACC-01]) trace to the spine ../../docs/0_use_cases.md.

1. Terminology

Shared commercial vocabulary (Offer, Module, Subscription, BillableEvent, Invoice, CreditNote, Refund) is defined once in the use-case spine. This crate adds the accounting-local terms:

2. This crate vs accounting_export

These two crates are easy to confuse and must never share data. The distinction is whose books:

Concernaccounting (this crate)business_domain/accounting_export
Whose booksGreen-Got’s own revenue / P&LA B2B customer’s own books
AudienceGreen-Got finance + Green-Got’s GLThe customer’s accountant / tooling
OutputRecognition, deferral, commission, VAT, reconciliation factsFEC, Pennylane, journal export of the customer’s transactions
SourceThe customer_billing subledger (Green-Got’s billing of customers)The customer’s banking transactions + their invoices
RelationGreen-Got is the seller recognising its own revenueGreen-Got is the service provider producing the customer’s export

Design rule — no shared tables. accounting and accounting_export share vocabulary (revenue, VAT, journal) but never share rows, stores, or migrations. A change here is never a change there. They sit in different domains (commercial_domain vs business_domain) precisely because one accounts for us and the other serves them.

3. Scope — what this crate owns

The crate is the revenue-recognition + GL-feed domain for Green-Got’s own income. It owns the derivation of:

Design rule — derive, do not author. This crate derives accounting facts from billing source data; it never authors a customer invoice, credit note, refund, or payment. Those are customer_billing’s ([UC-BIL-*]). Recognition runs off the recognition period, not the billing or collection date ([UC-ACC-01]).

4. Out of scope — the [UC-ACC-13] boundary

The commercial/billing domain produces accounting-source data: gross revenue, discounts / credit notes, VAT / tax code + rate, recognition period, receivable / payment state, refund / customer-credit liability, reason code, product / module reference, partner-commission events, and customer / legal-entity references. This crate consumes those source fields and computes recognition, deferral, accrual, provision and reconciliation.

It does not own:

Those live in a separate accounting integration layer fed by these source fields ([UC-ACC-13]). The worked contract is in 6. Reconciliation and Billing Source §4 and 7. Architecture §4.

Design rule — facts here, postings downstream. accounting emits account-neutral facts (amount, kind, period, tax, party, reason). It never names a GL account number or asserts a debit/credit pair. The GL-mapping layer turns a fact into balanced postings. This keeps the chart of accounts swappable without touching recognition logic.

Also out of scope: customer-facing fiscal reporting for savings (IFU / formulaire 2561), which is partner/insurer-owned ([UC-REG-07]); and the B2B-customer book export, which is accounting_export.

5. Invariants

6. Related documents

2. Data Model

Data Model

This document is the target-design data model for the accounting crate: the recognition entries, deferred-revenue / accrued-income projections, contra-revenue entries, partner-commission bookings, clawback schedules / events / provisions, and VAT policy. All amounts are integer cents (i64), EUR-only for now. Names are intended design, not committed schema (scaffolding only).

UC ids (e.g. [UC-ACC-01]) trace to the spine ../../docs/0_use_cases.md.

1. Design principles

2. Entities

2.1 RecognitionEntry

The core fact: one recognition schedule per billing line, carrying how its gross is earned over time.

FieldNotes
idrec_… typed id
billing_line_refthe customer_billing source line ([UC-ACC-13])
kindover_period ([UC-ACC-01]) | one_off_at_event ([UC-ACC-03]) | accrued_in_arrears ([UC-ACC-04])
gross_centsgross revenue of the line (before contra-revenue)
period_start, period_endthe recognition period; for one-off, start = end = event_date ([UC-ACC-03])
tax_code, vat_centssnapshotted tax code + resolved VAT ([UC-ACC-08])
legal_entity_ref, customer_refparty references for reconciliation ([UC-ACC-11])
product_refproduct/module reference ([UC-ACC-13])

The earned/deferred/accrued split at any cut date is derived from kind, the period, and the cut date (see 3. Revenue Recognition and Deferral).

2.2 Deferred revenue (PCA) and accrued income (FAE)

Not stored as standalone authoritative rows — they are projections over RecognitionEntry at a cut date ([UC-ACC-02]/[UC-ACC-04]/[UC-ACC-12]):

2.3 ContraRevenueEntry

A revenue reduction. Discounts are intrinsic; gestures are reason-code driven ([UC-ACC-05]/[UC-ACC-06]).

FieldNotes
idtyped id
billing_line_ref?the reduced line, when invoice-linked
reason_codemandatory; drives the treatment per the [UC-BIL-16] taxonomy
treatmentcontra_revenue | customer_credit_liability | expense ([UC-ACC-06])
amount_centsreduction amount

2.4 CommissionBooking

Partner-commission revenue for distributed savings — no customer invoice ([UC-ACC-07]).

FieldNotes
idcmb_… typed id
partner_ref, product_ref, policy_refwho / what / which contract
kindupfront | recurring_on_assets ([UC-ACC-07])
gross_centscommission earned from the partner event/statement
provision_centsexpected-clawback provision raised at booking ([UC-ACC-16]); 0 for recurring
net_centsgross − provision — the revenue actually recognised net of provision
basis, recognised_atthe accrual basis configured for this product, and when earned

2.5 ClawbackSchedule (reference data)

Per partner × product × commission-type. Nothing about clawback % / windows is hardcoded ([UC-ACC-14]).

FieldNotes
renunciation_window_dayswithin this window a renunciation is a 100% reversal ([UC-ACC-15])
reprise_periods[]ordered, contiguous, non-overlapping {from_month, to_month, clawback_pct}; final tier 0% = the point past which commission is unconditionally earned
clawback_basiswhat the reversal % applies to
settlement_modenetting (default) | invoice
per_transfer_triggers_clawbackPER flag: a transfer_out reverses the tier clawback_pct for the exit month

2.6 ClawbackEvent

The symmetric negative of a CommissionBooking ([UC-ACC-15]).

FieldNotes
idclw_… typed id
policy_ref, originating_commission_booking_idlinks back to what is being reversed
triggerpolicy_renounced | policy_surrendered_early | transfer_out
amount_centsthe reversal; draws down the provision if one exists ([UC-ACC-16])
periodthe period the reversal lands in (drives the netting line)

2.7 ClawbackProvision

The expected-clawback liability + its true-up ([UC-ACC-16]/[UC-ACC-17]).

FieldNotes
idprv_… typed id
partner_ref, product_ref, cohortthe true-up grouping
expected_ratefrom observed renunciation/surrender curves, versioned
held_centsthe liability currently held
versionthe curve version the rate came from

2.8 VatPolicy (effective-dated) and WriteOff

3. ER diagram

erDiagram
    RECOGNITION_ENTRY ||--o| CONTRA_REVENUE_ENTRY : "reduced by"
    RECOGNITION_ENTRY }o--|| VAT_POLICY : "resolves tax_code via"
    RECOGNITION_ENTRY ||--o| WRITE_OFF : "may be written off"
    COMMISSION_BOOKING }o--|| CLAWBACK_SCHEDULE : "governed by"
    COMMISSION_BOOKING ||--o{ CLAWBACK_EVENT : "reversed by"
    COMMISSION_BOOKING }o--o| CLAWBACK_PROVISION : "provisioned in"
    CLAWBACK_EVENT }o--|| CLAWBACK_PROVISION : "draws down"
    CLAWBACK_SCHEDULE ||--o{ CLAWBACK_PROVISION : "cohort rate"

    RECOGNITION_ENTRY {
        id rec_id
        ref billing_line_ref
        enum kind
        i64 gross_cents
        date period_start
        date period_end
        string tax_code
        i64 vat_cents
    }
    CONTRA_REVENUE_ENTRY {
        id contra_id
        ref billing_line_ref
        string reason_code
        enum treatment
        i64 amount_cents
    }
    COMMISSION_BOOKING {
        id cmb_id
        ref partner_ref
        ref product_ref
        ref policy_ref
        enum kind
        i64 gross_cents
        i64 provision_cents
        i64 net_cents
    }
    CLAWBACK_SCHEDULE {
        ref partner_ref
        ref product_ref
        string commission_type
        i32 renunciation_window_days
        json reprise_periods
        enum settlement_mode
        bool per_transfer_triggers_clawback
    }
    CLAWBACK_EVENT {
        id clw_id
        ref policy_ref
        ref originating_commission_booking_id
        enum trigger
        i64 amount_cents
        period period
    }
    CLAWBACK_PROVISION {
        id prv_id
        ref partner_ref
        ref product_ref
        string cohort
        decimal expected_rate
        i64 held_cents
        i32 version
    }
    VAT_POLICY {
        string tax_code
        date effective_from
        date effective_to
        enum treatment
        decimal rate
        string statutory_ref
    }
    WRITE_OFF {
        id write_off_id
        ref invoice_ref
        i64 loss_cents
        date at
    }

4. Invariants

5. Related documents

3. Revenue Recognition and Deferral

Revenue Recognition and Deferral

This document specifies how the accounting crate recognises Green-Got’s revenue over time: spread over the service period ([UC-ACC-01]), deferred for annual prepaid subscriptions ([UC-ACC-02]), recognised immediately for one-off fees ([UC-ACC-03]), accrued when billed in arrears ([UC-ACC-04]), and positioned correctly at period close ([UC-ACC-12]). All amounts are integer cents.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. The principle — earn over the recognition period

Billing, recognition and payment are three distinct concerns (Appendix A locked premise). A recurring subscription fee is billed in advance ([UC-BIL-01]), but the revenue is earned over the period it covers. The RecognitionEntry.recognition_period (carried on the billing source line) drives recognition — never the billing date and never the collection date.

Design rule — recognition runs off the period, not the cash. Revenue earned to a cut date d is a function of kind, [period_start, period_end], gross_cents, and d only. When the cash clears is irrelevant to recognition ([UC-ACC-01]/[UC-ACC-03]).

2. Recognition kinds

kindWhen recognisedSource caseUC
over_periodProrata temporis across [period_start, period_end]recurring subscription billed in advance[UC-ACC-01]
one_off_at_eventFully at event_date (period_start = period_end)ATM / FX / reissue fee[UC-ACC-03]
accrued_in_arrearsEarned as service is rendered, before the invoice existsservice billed after the fact[UC-ACC-04]

3. Deferred revenue (PCA) — annual prepaid

An annual fee billed in advance is earned across the 12 sub-periods it covers. At the end of month 1, ~1/12 is recognised and ~11/12 sits in deferred revenue (PCA — produits constatés d’avance), a liability that releases prorata as the period elapses ([UC-ACC-01]/[UC-ACC-02]).

Design rule — PCA is derived, not booked separately. Deferred revenue at any date is the unearned remainder of the over_period entries, computed from the schedule; it is a projection, not an authoritative balance ([UC-ACC-02]). See 2. Data Model §2.2.

Worked example (annual €120.00 fee, billed day 1, 12 monthly sub-periods):

At end of monthRecognised (cumulative)Deferred (PCA)
1€10.00€110.00
6€60.00€60.00
12€120.00€0.00

4. One-off fees — recognised at the event date

An ATM / FX / reissue fee is recognised in full at the event date, with recognition_period_start = end = event_date, even if the cash clears in a later period ([UC-ACC-03]). No deferral, no accrual.

5. Accrued income (FAE) — billed in arrears

When a service is rendered before it is invoiced, the earned amount is accrued income (FAE — facture à établir), an asset, recognised in the period the service was rendered. The line-level recognition period drives the correct month, so the accrual lands in the right period regardless of when the invoice is later issued ([UC-ACC-04]). When the invoice is finally raised, the FAE is reversed against the now-real receivable.

6. Period close / cut-off

At month/year close, the rattachement (period attribution) is already correct from the line-level recognition periods: deferred revenue and accrued income are already positioned, so there is no manual cut-off for these lines ([UC-ACC-12]).

flowchart TD
    A[customer_billing line
gross + recognition_period] --> B{RecognitionEntry.kind} B -->|over_period| C[spread prorata across period] B -->|one_off_at_event| D[recognise fully at event_date] B -->|accrued_in_arrears| E[accrue as service rendered] C --> F[Period close cut-off at date d] D --> F E --> F F --> G[Recognised revenue this period] F --> H[Deferred revenue PCA
unearned remainder] F --> I[Accrued income FAE
earned-not-billed] G --> J[account-neutral facts → GL-mapping layer UC-ACC-13] H --> J I --> J

Design rule — close is a read, not a posting. Period close computes the recognised / PCA / FAE split as a read model over the recognition entries at the cut date; it emits account-neutral facts to the GL-mapping layer and never writes balanced journal postings itself ([UC-ACC-12]/[UC-ACC-13]).

7. Invariants

8. Related documents

4. Partner Commission and Clawback

Partner Commission and Clawback

This document specifies how the accounting crate recognises partner-commission revenue for distributed savings ([UC-ACC-07]), the clawback schedule reference data that governs reversals ([UC-ACC-14]), the clawback / reversal event on renunciation or early surrender ([UC-ACC-15]), the expected-clawback provision raised at booking ([UC-ACC-16]), and its periodic true-up ([UC-ACC-17]). All amounts are integer cents.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Commission revenue — no customer invoice

Savings / ASV / PER / livrets are intermediated, free, commission-funded (Appendix A locked premise). They bill the customer nothing ([UC-BIL-15]); the only economic event is commission paid by the partner. This is a distinct revenue line from subscription fees and lives only in accounting — there is no customer_billing invoice ([UC-ACC-07]).

Design rule — commission lives outside customer_billing. A CommissionBooking references the partner, product and policy, not a customer invoice. Its source is the partner commission event / statement / accrual basis configured for that product, not a billed line ([UC-ACC-07]).

Commission may be upfront at subscription and/or recurring on outstanding assets, depending on the partner contract / product. It is recognised when earned from the configured basis.

2. Clawback exposure on upfront commission

Upfront commission is exposed to clawback (reprise) if the customer renounces or surrenders early. The design recognises upfront commission net of an expected-clawback provision ([UC-ACC-16]) and fires the symmetric reversal event on exit ([UC-ACC-15]). Recurring-on-assets commission carries no provision (nothing to claw back beyond what is yet to be earned).

3. Clawback schedule — reference data ([UC-ACC-14])

Per partner × product × commission-type, the schedule holds the terms downstream reads from. Nothing about clawback % or windows is hardcoded — it is all reference data:

4. Clawback / reversal event ([UC-ACC-15])

The clawback event is the symmetric negative of a commission booking and must exist whatever the recognition policy — because the insurer’s netting has to land in the ledger regardless. Triggers:

TriggerEffect
policy_renounced (within renunciation_window_days)100% reversal
policy_surrendered_earlyreverse the tier clawback_pct for the exit month per [UC-ACC-14]
transfer_out (PER, where per_transfer_triggers_clawback)reverse the tier clawback_pct for the exit month

It posts contra-revenue, or draws down the provision when [UC-ACC-16] is live. The event carries policy_ref + originating_commission_booking_id; under netting, the period’s events sum to the partner statement’s negative line (any residual is the settlement variance).

5. Expected-clawback provision at booking ([UC-ACC-16])

At booking, recognise the full upfront commission and simultaneously raise a clawback_provision liability:

clawback_provision = expected_clawback_rate × commission
net_recognised     = commission − clawback_provision

The expected_clawback_rate comes from observed renunciation / surrender curves per partner × product, and is versioned. Actual exits draw down the provision rather than taking a fresh revenue hit. This is the IFRS 15 variable-consideration constraint / French-GAAP prudence applied — recognise only to the extent it is highly probable it will not reverse.

6. Periodic provision true-up ([UC-ACC-17])

Monthly, aligned to the partner settlement cycle, per partner × product cohort:

  1. Compare cumulative actual clawbacks ([UC-ACC-15]) to the provision held.
  2. Re-estimate the rate from trailing actuals (a new versioned curve).
  3. Adjust the provision — an increase posts contra-revenue; a release posts revenue.
  4. When a policy passes the final tier (0%), release its remaining provision — the commission is now unconditionally earned.
flowchart TD
    A[Partner commission event] --> B{kind}
    B -->|recurring_on_assets| C[recognise gross when earned
no provision] B -->|upfront| D[recognise gross] D --> E[raise clawback_provision
= expected_rate × commission UC-ACC-16] E --> F[net_recognised = gross − provision] G[Exit event UC-ACC-15] -->|renounced / surrendered / transfer_out| H{provision live?} H -->|yes| I[draw down provision] H -->|no| J[post contra-revenue] K[Monthly true-up UC-ACC-17] --> L[compare actuals vs held] L --> M{adjust} M -->|increase| N[contra-revenue] M -->|release / passed 0% tier| O[revenue] I --> P[netting → partner statement negative line] J --> P

7. Settlement

8. Invariants

9. Open items

The per-product commission trigger and accrual basis (which products are upfront vs recurring, and the exact event/statement/accrual that recognises each) is not yet pinned — see Uncertainties ACC-07.

10. Related documents

5. VAT and Contra-Revenue

VAT and Contra-Revenue

This document specifies how the accounting crate determines VAT on Green-Got’s own revenue ([UC-ACC-08]), recovers VAT on credit notes ([UC-ACC-09]), and treats discounts ([UC-ACC-05]) and reason-coded commercial gestures ([UC-ACC-06]) as contra-revenue. All amounts are integer cents.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. VAT is tax-code driven, effective-dated ([UC-ACC-08])

VAT is not hard-coded by the catalogue. Each product / fee carries a dated tax_code; VAT is resolved at issuance from the tax_code + a dated policy / rate table + territory, and the result is snapshotted on the line. The policy can change over time (for example if Green-Got renounces the 260 B option) without rewriting history.

Design rule — resolve once, snapshot. VAT is resolved from tax_code + the effective-dated VatPolicy (2. Data Model §2.8) at issuance and stored on the recognition entry; later policy changes never retroactively alter a snapshotted line ([UC-ACC-08]).

1.1 Finance baseline (provisional)

RevenueTreatmentNotes
Core banking / payment servicesExemptwithin the art. 261 C exemption perimeter
Green-Got subscription feeTaxable @ 20% under the art. 260 B option⚠️ provisional — needs tax-advisor validation (see §1.2)
Non-banking service feesTaxable by defaultunless their own tax_code says otherwise
Partner commissions (ASV/PER/livrets)ExemptVAT-exempt intermediation revenue (see §3)
TCA (where relevant)Borne by insurer/partnerASV / capitalisation expected outside Green-Got TCA handling

1.2 The 260 B option — not a settled fact ⚠️

The subscription’s taxable-at-20%-under-260 B treatment is provisional. The 260 B option is irrevocable for 5 years, and its scope over a payment-account subscription fee (vs the 261 C exemption of the payment services themselves) must be confirmed by a tax advisor before being baked into accounting. This is tracked as a launch-blocking item — see Uncertainties ACC-08. Until validated, the tax_code → policy mapping treats it as a configurable, reversible decision, not a constant.

2. Credit note reverses revenue + recovers VAT ([UC-ACC-09])

A credit note reverses the line’s revenue and the associated collected VAT. It is the revenue-reducing counterpart to recognition: the recognised gross and its snapshotted VAT are both backed out for the credited amount.

Design rule — symmetric reversal. A credit note reverses both the revenue and the VAT snapshotted on the original line, using that line’s tax treatment — not the current policy ([UC-ACC-09]).

3. VAT on partner commission

Partner commissions on ASV / PER / livrets are VAT-exempt intermediation revenue ([UC-ACC-08]). They carry no output VAT; the commission booking (4. Partner Commission and Clawback) records exempt revenue.

4. Contra-revenue — discounts and gestures

4.1 Discounts / promos ([UC-ACC-05])

Offer-intrinsic discounts and invoice-reducing commercial gestures reduce revenue: the billing line carries a gross line + a discount / credit contra-line, preserving the gross-revenue vs discount distinction ([UC-BIL-03]). They are not marketing expenses unless they remunerate a distinct service — which is outside the current commercial-billing model.

Design rule — discount is contra-revenue, not expense. A discount reduces recognised revenue; it is never reclassified as a marketing cost within this model ([UC-ACC-05]).

4.2 Commercial gestures are reason-code driven ([UC-ACC-06])

Treatment is reason-code driven: each reason code maps to one of contra-revenue (credit note) / customer-credit liability / expense, per the canonical taxonomy in [UC-BIL-16]. The reason code is mandatory — it determines the GL treatment (revenue vs liability vs expense) and supports audit.

The [UC-BIL-16] taxonomy this crate consumes:

Source case (reason class)Accounting treatment
service not rendered (e.g. card not delivered)credit note reducing revenue; if already collected, a refund liability until paid
gesture reducing an invoicecredit note reducing revenue
cashback / bonus / reward not linked to an invoicecustomer-credit / pending-balance liability (not an invoice credit note)
pure compensation / indemnity not reducing a priceexpense, with a customer liability until settled
billing error / double chargecorrection credit note + refund liability if already collected

Design rule — the reason code, not the channel, sets the treatment. Each ContraRevenueEntry carries a mandatory reason_code; its treatment (contra_revenue / customer_credit_liability / expense) is the [UC-BIL-16] mapping of that code, never chosen independently ([UC-ACC-06]). The exact reason-code taxonomy is being finalised — see Uncertainties ACC-06.

5. Invariants

6. Related documents

6. Reconciliation and Billing Source

Reconciliation and Billing Source

This document specifies how the accounting crate handles write-offs ([UC-ACC-10]), the receivables↔GL reconciliation ([UC-ACC-11]), and the billing-source-data vs GL-mapping boundary ([UC-ACC-13]) — the contract for what this crate consumes and where the downstream GL-mapping layer begins. All amounts are integer cents.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Write-off → loss, revenue kept ([UC-ACC-10])

A write-off ([UC-BIL-10]) of an uncollectable receivable keeps the original revenue and recognises a loss (créance irrécouvrable). It is not a revenue reversal — the sale happened and the revenue stays; the loss reflects the failure to collect.

Design rule — write-off is a loss, not a reversal. A WriteOff recognises loss_cents and never reduces the original recognised revenue. (Contrast with a credit note ([UC-ACC-09]), which does reverse revenue.) The two must not be conflated.

EventRevenueLoss / VAT
Credit note ([UC-ACC-09])reversedVAT recovered
Write-off ([UC-ACC-10])keptloss recognised; revenue unchanged

2. Receivables ↔ GL reconciliation ([UC-ACC-11])

At any date, the open receivables must tie to the GL:

Σ open invoice balances  =  GL client-receivable balance

This reconciliation supports aged-balance buckets and doubtful-debt provisioning, and feeds RUBA / ACPR prudential inputs.

Design rule — receivables tie out at every cut. The reconciliation is a read model: it sums the open invoice balances from the billing source data and asserts equality with the GL client-receivable balance. A discrepancy is an anomaly to surface, never silently reconciled ([UC-ACC-11]).

flowchart LR
    A[customer_billing
open invoice balances] --> B[Σ open balances] B --> C{= GL client-receivable?} C -->|yes| D[reconciled] C -->|no| E[anomaly surfaced] D --> F[aged buckets + doubtful-debt provision] F --> G[RUBA / ACPR inputs]

3. Period close interplay

Reconciliation runs over the same cut date as period close (3. Revenue Recognition and Deferral §6): recognised revenue, deferred revenue (PCA) and accrued income (FAE) are positioned by recognition period, while the receivables↔GL tie-out validates the asset side. Both emit account-neutral facts to the GL-mapping layer.

4. The [UC-ACC-13] boundary

The commercial / billing domain produces accounting-source data; this crate consumes it and derives recognition / deferral / accrual / commission / provision / reconciliation facts. The chart-of-accounts mapping and double-entry journal generation live in a separate accounting integration layer fed by these fields.

4.1 Source fields this crate consumes (produced by customer_billing)

4.2 What this crate does not own

flowchart LR
    subgraph CB[customer_billing - subledger]
        S[accounting-source fields
gross, discount, tax_code, recognition_period,
payment state, reason_code, refs, commission events] end subgraph ACC[accounting - this crate] R[recognition / deferral / accrual] K[commission + clawback + provision] V[VAT determination] X[reconciliation + write-off] end subgraph GL[GL-mapping integration layer - separate] M[chart-of-accounts mapping] J[double-entry journal generation] end S --> R & K & V & X R & K & V & X -->|account-neutral facts| M --> J

Design rule — facts here, accounts and postings downstream. This crate emits account-neutral facts (amount, kind, period, tax, party, reason); it never names a GL account or asserts a debit/credit pair. The GL-mapping layer turns each fact into balanced postings ([UC-ACC-13]). Ownership of that layer is still open — see Uncertainties — GL-mapping-layer ownership.

5. Invariants

6. Related documents

7. Architecture

Architecture

This document describes the intended DDD architecture of the accounting crate: its layers, its segment-neutrality, how it consumes customer_billing source data, and where the [UC-ACC-13] GL-mapping boundary begins. Scaffolding only — the structure here is design intent, mirrored in plan.md.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Shape — single crate, shallow DDD

accounting is derive-and-read logic over billing source data plus partner-commission reference data. It has no customer-facing routes and issues no invoices, so it stays a single crate with a shallow DDD layout per the architecture skill; layers promote to full folders only as complexity grows.

LayerContentsNotes
domain/recognition, deferral, contra-revenue, commission, clawback, VAT, reconciliation value objects + invariantszero cross-layer deps; no GL-mapping knowledge ([UC-ACC-13])
use_cases/recognise line, close period, book/claw commission, true-up, determine VAT, reconcilepure given source data; orchestration only
stores/persistence of derived facts + reference data (vat_policies, clawback_schedules)*Record structs private to the layer
rules/eventbus subscribers translating customer_billing / partner events into use-case callsthe inbound seam

2. Segment-neutral

Per the Appendix A locked premise, accounting is segment-neutral data + logic alongside offers, subscriptions and customer_billing: it imports no segment systems. Retail vs Business is a plain field on the source data, applied by readers — never a code dependency. This keeps the recognition, commission and VAT logic identical across segments.

3. Consuming customer_billing source data

The crate is fed by the customer_billing subledger. The seam is the rules/ layer:

flowchart TD
    subgraph CB[customer_billing]
        E1[BillingLineCreated]
        E2[CreditNoteIssued]
        E3[WriteOff UC-BIL-10]
        E4[PaymentStateChanged]
    end
    subgraph P[partners / investment]
        E5[CommissionEvent]
        E6[PolicyExit renounce/surrender/transfer]
    end
    subgraph ACC[accounting]
        direction TB
        RU[rules/] --> UC[use_cases/]
        UC --> DM[domain/]
        UC --> ST[stores/]
    end
    E1 & E2 & E3 & E4 --> RU
    E5 & E6 --> RU
    ACC -->|account-neutral facts| GL[GL-mapping layer UC-ACC-13]

Design rule — react, don’t poll. Recognition is driven by customer_billing and partner events through rules/; the crate does not reach into another domain’s tables ([UC-ACC-13]). It copies only the snapshotted source values it needs to be auditable.

4. The [UC-ACC-13] boundary

The architectural line is sharp: accounting ends at account-neutral facts; the GL-mapping layer begins at accounts and postings.

Design rule — no account numbers in domain/. No domain type, store row, or emitted fact names a GL account or asserts a debit/credit pair. This keeps the chart of accounts swappable and the GL-mapping layer independently ownable (ownership of that layer is open — see Uncertainties — GL-mapping-layer ownership).

5. What lives elsewhere

ConcernOwner
Billing lines, invoices, credit notes, refunds, customer-credit liabilities, tax-code snapshots, reason codes, recognition periodscustomer_billing
Savings / ASV / PER / livret contracts + partner-commission eventsinvestment + partners
Chart-of-accounts mapping + double-entry journalseparate GL integration layer ([UC-ACC-13])
B2B-customer book export (FEC / Pennylane)business_domain/accounting_export (different domain)
Customer fiscal documents (IFU / 2561)partner / insurer ([UC-REG-07])

6. Invariants

7. Related documents

Uncertainties

Accounting — Active Register

This is an active, classified register of the open items in the accounting (GL-view) domain. The revenue-recognition model is settled and documented (recognition over period, PCA / FAE, one-off at event, contra-revenue, partner commission + clawback + provision, write-off, reconciliation, period close); this register tracks the remaining legal/validation, product-decision and ownership items until each is closed.

Status sections. Items move through: §1 Research / validation pending (delegated — investigate or get tax-advisor sign-off, then close), §2 Resolved decisions (decided here; awaiting port into the named canonical doc), §3 Closing items (how each closes).

Convention. “Launch-blocking = yes” means the GL feed cannot be relied on for a real customer’s revenue until the item is closed. A resolved item that is launch-blocking stays launch-blocking until ported into its canonical doc.


1. Research / validation pending

ACC-08 — 260 B option validation + tax-code table

Question: Is Green-Got’s subscription fee correctly treated as taxable @ 20% under the art. 260 B option, given that the 260 B option is irrevocable for 5 years and that the underlying payment services sit in the art. 261 C exemption perimeter? And what is the full effective-dated tax_code{treatment, rate, statutory_ref} table that the VatPolicy store must hold? The baseline in 5. VAT and Contra-Revenue §1.1 is provisional, not a settled fact — it must be confirmed by a tax advisor before being baked in.

ACC-07 — Per-product commission trigger / accrual basis

Question: Per partner × product, which commission is upfront vs recurring on outstanding assets, and exactly which event / statement / accrual basis recognises each? The clawback mechanics are documented (4. Partner Commission and Clawback); only the per-product trigger + accrual basis mapping (the CommissionBooking.kind and basis per product) remains to be pinned against the actual partner contracts.

ACC-06 — Reason-code taxonomy

Question: The exact reason-code taxonomy that drives contra-revenue vs customer-credit liability vs expense. The five [UC-BIL-16] classes are documented (5. VAT and Contra-Revenue §4.2); the precise enumerated code set (the canonical strings, one per case, shared with customer_billing audit) must be fixed so the ContraRevenueEntry.reason_code → treatment mapping is total and unambiguous.


2. Resolved decisions (awaiting port into canonical docs)

GL-mapping-layer ownership

Item id: ACC-MAP. Question / decision needed: who owns the separate accounting integration layer that holds the chart-of-accounts mapping and double-entry journal generation fed by this crate’s account-neutral facts ([UC-ACC-13])? The boundary is decided and documented (this crate ends at facts; the GL layer begins at accounts + postings — 7. Architecture §4). What remains is the ownership / location decision: a sibling crate, an external finance tool, or a later phase.


3. Closing items

An item is closed by recording the confirmed value / decision in the owning accounting doc. ACC-08 additionally requires tax-advisor sign-off before the tax_code → policy table is treated as authoritative; until then the 260 B treatment stays configurable and reversible. ACC-08 and ACC-07 gate a reliable GL feed and are launch-blocking.

4. Related documents

Checkout / PVID

0. Integration Overview

Checkout (Ubble) PVID — Integration Documentation

This is the internal reference for Green-Got’s integration with Ubble (now Checkout.com – Identity Verification) for remote identity verification used as PVID during KYC at onboarding. It describes what the vendor does, how we talk to them, what we store, and how the pieces fit our codebase. It is the source of truth for the checkout_pvid crate and the clients::checkout client.

Vendor naming. The product is historically Ubble; Ubble was acquired by Checkout.com and is now branded “Checkout.com – Identity Verification”. Everything still runs on ubble.ai domains, ubble.ai IDs, and the Ubble dashboard. Docs use both names interchangeably; HTTP headers are prefixed Cko- (Checkout.com). We call our integration “checkout” internally (matches the legacy @green-got/checkout package) and “PVID” for the regulated purpose.


1. What Ubble does and why we need it

Ubble’s “Identities” platform verifies that a real, live person holds a genuine identity document. The customer records a video capture of an ID document (front/back) and a video/selfie of their face; Ubble runs document-authenticity, data-extraction, face-match and liveness checks, then returns a decision (approved / declined / …) plus the extracted identity (names, DOB, document number, MRZ, nationality, …).

We need it as the id_doc_pvid onboarding step: a customer cannot pass KYC and be provisioned an account without a successful identity verification. For Green-Got this must use Ubble’s Certified / PVID processing configuration — “Certified by the French Government for merchants with a French banking license” (the ANSSI / French-state-certified remote-IDV scheme, PVID = Prestataire de Vérification d’Identité à Distance). Certified verifications add document/face “challenges” (timeouts) and return an extra verification_policy_version field we must persist as compliance evidence.

Ubble offers four products; we use only IDV (Identity Verification). The others — ID Document Verification (image-only, “coming soon”), AML Screening (PEP/sanctions), Face Authentication (re-auth a returning user) — are out of scope unless explicitly added later.


2. Where this lives in the codebase (placement decision)

The user’s first instinct was src/clients/src/checkout_pvid. We split it instead, because the clients crate is a flat collection of stateless, thin third-party API wrappers (yousign, insee, efficiale, …) with no persistence and no business logic. Our PVID needs a local datamodel, stores, a webhook state-machine, KYC linkage and back-office exposure — that is a domain, not a client.

LayerLocationOwns
Transport clientsrc/clients/src/checkout/mTLS + Basic-auth HTTP client, v2 request/response DTOs, Cko-Signature verification primitive. Pure I/O. Mirrors the yousign submodule.
Domain cratesrc/commercial_domain/checkout_pvid/Local datamodel (mirror of all Ubble data), stores, use-cases (start / handle-webhook / refresh / archive assets / back-office queries), the status machine, and the IdentityVerificationProvider implementation onboarding consumes.
Migrationssrc/db/migrations/2026…_checkout_pvid.{up,down}.sqlThe local mirror tables.
Back-office APIsrc/services/back_office_api/src/routes/#[handler(domain = "crm")] read/manage endpoints under /crm/checkout_id_verification.
Parameterssrc/env/src/parameters/checkout.rsmTLS cert/key, Basic-auth client_id/client_secret, signature public keys, user_journey_id, API URL — per environment, via #[secret] + gg.

Co-located under commercial_domain (next to onboarding) because PVID is consumed by commercial onboarding KYC. The transport client follows repo convention that all third-party HTTP clients live in src/clients; the domain crate depends on it.


3. The integration seam (onboarding) — and the async mismatch

Onboarding already declares the seam in onboarding/src/infrastructure/adapters/identity_verification.rs:

pub trait IdentityVerificationProvider: Send + Sync {
    async fn verify(&self, subject: &IdentitySubject) -> eyre::Result<IdentityOutcome>;
}
// IdentityOutcome = Verified | Failed | Pending

The id_doc_pvid step calls verify() synchronously inside on_complete and writes IdentityVerificationStatus = "Verified" only on Verified; otherwise the step stays unmet and the resolver re-surfaces it.

⚠️ This synchronous shape does not fit Ubble. Ubble is asynchronous and webhook-driven: you create a verification, redirect the user to a hosted URL, and learn the result later via webhook. A single blocking verify() cannot model “user is now off doing a capture; come back in a few minutes.”

Design decision (to validate with the user): the seam must evolve to a start + poll/await shape, e.g.:

The RealIdentityProvider stub in onboarding (bail!("…not wired yet")) is the slot the checkout_pvid implementation plugs into. The provider selector currently keys off ONBOARDING_PVID_PROVIDER=real|sandbox.


4. Authentication & environments

There is one base URL for both prod and test: https://api.ubble.ai. There is no separate sandbox host — environment is decided by which credentials you present. Health probe: GET /health.

Every API call needs two layers:

  1. mTLS (mutual TLS) client certificate — the API certificate (public cert + private key) presented in the TLS handshake. One identity per environment (prod / test). Use the existing mtls crate (MtlsClientConfig + HttpMtls trait) to build the reqwest::Client.
  2. HTTP Basic authclient_id / client_secret in the Authorization header, per environment.

Status: the mTLS API certificate (cert + key) and the webhook signature public keys are now encrypted with gg env encrypt and pushed into parameters/checkout.rstest identity for development/staging, production identity for production. The env crate compiles and round-trips.

mTLS alone authenticates (verified 2026-06-30). A live probe with the test client certificate proved it: GET /v2/applicants returns 401 with no client cert but 405 (method-not-allowed — past the auth gate) when the test cert is presented. So the HTTP Basic client_id/client_secret the docs mention are not required for our account; they remain Secret::todo() and the client sends them anyway (empty, harmless).

user_journey_id provisioned (2026-06-30): the “Green-Got” journey usj_01h13smkbjh2x48zn7v6mkp8g0 is set in all environments. With mTLS auth working and the journey id in place, all credentials needed to run the full create → verification → attempt flow are present.

There is no OAuth and no plain API-key header.

Webhook signature keys (637-prod-v1, 637-test-v1) are inbound only — used to verify signatures Ubble produces on webhooks (§7). They are never sent on requests. The key id decodes as <org=637>-<env>-<version>.

Versioning: the API is v2 (info.version: 2.0.0); all IDV paths are path-versioned under /v2/. No version header. Do not use the legacy v1 API. Response codes are explicitly additive/backward-compatible — build business rules on status, treat response_codes as additional info, and never hard-fail on an unknown code.


5. The verification flow

We use the explicit three-step flow (not the create-and-start-idv shortcut), because onboarding needs the applicant linkage and per-attempt retry control:

  1. Create / reuse applicantPOST /v2/applicantsapplicant_id (aplt_…). One applicant per person, reused across verifications. We store the applicant_id against the user/onboarding owner. In non-prod, magic external_applicant_id values drive deterministic outcomes (§9).
  2. Create identity verificationPOST /v2/identity-verifications with applicant_id, declared_data {name, birth_date?}, webhook_url, user_journey_id (= our Certified/PVID config). Returns idv_…, status created. Declared name should be the legal name if the user has updated legal data (Ubble matches the captured document against the declared name).
  3. Create attemptPOST /v2/identity-verifications/{idv}/attempts with redirect_url (+ optional phone_number, client_information to pre-select country/doc-type/language). Returns iatp_… and _links.verification_url.href, status pending. The verification URL expires after 15 minutes — create a fresh attempt each time the user (re)enters the flow.
  4. User captures at the hosted web app https://id.ubble.ai/<token>/ (or embedded via the iframe/webview SDK), then is sent to our redirect_url.
  5. Ubble processes (status checks_in_progress) and fires webhooks at each step.
  6. On completion (identity_verification_checks_completed): we call GET /v2/identity-verifications/{idv} to fetch the full extracted data, and …/attempts/{iatp}/assets for the captured media URLs (which we must download promptly — they expire in ~1h). identity_verification_report_created signals the PDF report is ready.

One applicant → one (or more) verifications; one verification → one or more attempts. A declined verification is terminal (to retry you create a new verification); a retry_required verification accepts a new attempt on the same idv.


6. API endpoints we use

Base https://api.ubble.ai, all application/json, IDV paths under /v2.

MethodPathPurpose
POST/v2/applicantsCreate applicant (external_applicant_id?, external_applicant_name?, email?).
GET/v2/applicants/{id}Retrieve applicant.
POST/v2/applicants/{id}/anonymizeGDPR erase.
POST/v2/identity-verificationsCreate verification.
GET/v2/identity-verifications/{id}Retrieve verification + all extracted results (main read).
POST/v2/identity-verifications/{id}/attemptsCreate attempt / retry.
GET/v2/identity-verifications/{id}/attemptsList attempts (paginated).
GET/v2/identity-verifications/{id}/attempts/{aid}Retrieve one attempt.
GET/v2/identity-verifications/{id}/attempts/{aid}/assetsCaptured media pre-signed URLs (~1h TTL).
GET/v2/identity-verifications/{id}/pdf-report{ "pdf_report": "<url>" }.
POST/v2/identity-verifications/{id}/notifyRe-fire the webhook for the current state (testing/integration).
POST/v2/identity-verifications/{id}/anonymizeGDPR anonymize.

List endpoints paginate with total_count / skip / limit and HAL _links (self/next/previous). IDs are {prefix}_{base32 guid}: aplt, idv, iatp, usj, evnt.


7. Webhooks & signature verification

Registration: none — pass webhook_url in the create-verification body; you get a callback for every event on that verification.

Delivery: respond 200/201 within 10 seconds or Ubble retries up to 2×. User-Agent CkoIdvNotifier; source IPs in OUTSCALE cloudgouv-eu-west-1 (FR sovereign cloud — relevant for us). Re-fire manually via /notify. Ack fast, process async, and dedupe on the CloudEvents evnt_… id (retries can re-deliver).

Payload = CloudEvents 2.0 envelope:

{
  "specversion": "2.0",
  "type": "identity_verification_checks_completed",
  "subject": "idv_…",
  "id": "evnt_…",
  "time": "2023-03-22T17:31:00Z",
  "datacontenttype": "application/json",
  "data": {
    "applicant_id": "aplt_…",
    "external_applicant_id": "",
    "user_journey_id": "usj_…",
    "identity_verification_id": "idv_…",
    "status": "declined",
    "response_codes": [ {"code": 62301, "summary": "document_counterfeit"} ]
  }
}

The webhook body does not carry the full extracted identity — on a terminal event we must GET /v2/identity-verifications/{id}.

Event types (IDV): identity_verification_ + created, opened, started, reset, capture_completed, checks_completed, link_expired, capture_refused, capture_aborted, checks_inconclusive, retry_requested, closed, anonymized, audit_completed (status can change after a post-hoc audit — must be handled), report_created.

Cko-Signature verification (critical, partially under-documented)

Receiving webhooks locally (Tailscale Funnel)

Ubble has no dashboard webhook configuration — the webhook_url is supplied per verification in the create-verification body (above). So to receive webhooks on a dev machine you expose your local webhook endpoint to the public internet and pass that URL as webhook_url.

The inbound route is POST /checkout/webhook (the checkout_webhook service, mounted under /checkout), served by the local backend on :8080http://localhost:8080/checkout/webhook.

We use Tailscale Funnel (we already run Tailscale) to expose it publicly:

  1. On macOS the CLI ships inside the app bundle — alias it once (on Linux tailscale is already on PATH):

    alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
    
  2. Find your node’s MagicDNS name (the public host) in the Self row of tailscale status — e.g. your-machine.tailnet-XXXX.ts.net.

  3. Start the backend (gg dev, serving :8080), then expose it:

    tailscale funnel 8080        # add --bg to background it; `tailscale funnel --bg off` to stop
    

    Funnel proxies public https://<node>.<tailnet>.ts.net (:443) → local :8080.

    ⚠️ This exposes the WHOLE backend port publicly, not just /checkout/webhook. In dev, the private and public routers share the :8080 listener, and the back-office dev auth bypass is default-on. The bypass is hard-constrained to loopback-origin requests (it refuses anything carrying a proxy/tunnel forwarding header, which Funnel always injects — see internal_access::session_auth), so a funneled /back_office/... request falls through to real passkey auth rather than getting a synthetic super-admin session. Keep the Funnel up only while actively testing webhooks, and prefer tailscale funnel --bg off as soon as you’re done.

  4. Your webhook URL is then:

    https://<node>.<tailnet>.ts.net/checkout/webhook
    

    Pass it as webhook_url on create-verification; re-fire events on demand with POST /v2/identity-verifications/{id}/notify (§6).

  5. Confirm with tailscale funnel status (should show :443 → 127.0.0.1:8080).

Prerequisites / gotchas:


8. Statuses & lifecycle

Verification status (status on the IDV resource):

createdpendingcapture_in_progresschecks_in_progress → terminal: approved / declined / retry_required / refused / inconclusive.

StatusMeaningTerminal?
createdVerification created, no attempt yet.no
pendingAttempt exists, verification_url available.no
capture_in_progressApplicant is capturing.no
checks_in_progressCapture done, checks running.no
approvedVerified OK.yes
declinedIrregularity/fraud found. Retry ⇒ new verification.yes
retry_requiredCouldn’t complete checks. Retry ⇒ new attempt on same idv.recoverable
refusedApplicant explicitly refused.yes
inconclusiveChecks incomplete & retry no longer available.yes

Attempt status (AttemptStates): pending_redirection, capture_in_progress, capture_aborted, capture_refused, expired, checks_in_progress, completed, checks_inconclusive, terminated (superseded by a newer attempt).

Event → status mapping (IDV):

Map to onboarding IdentityOutcome: approvedVerified; declined/refused/ inconclusiveFailed; everything else ⇒ Pending.


9. Data we store (local mirror — store everything)

Principle: duplicate all content Ubble gives us locally, so we can operate (and back-office can review) even if Ubble changes their API or we need offline processing. All non-required fields are Option<T>; store raw response_codes ints even when the summary is unknown.

From GET /v2/identity-verifications/{id} (IdentityVerificationOutputGet):

From GET …/attempts/{id}: id (iatp_…), created_on, modified_on, status, response_codes[], phone_number, redirect_url, client_information (pre_selected_*), applicant_session_information (ip_address, number_of_sessions, user_agent, initial_device mobile/desktop, selected_documents[] {country, document_type}), _links.

From GET …/attempts/{id}/assets: media items { type, _links.asset_url.href } where typeface_image, face_video, document_front_image/_video, document_back_image/_video, document_signature_image (+ secondary_document_*).

No numeric scores. v2 exposes a decision (status) + categorical response_codes + risk_labels, not raw face-match/liveness confidence scores. Model the datamodel around codes + status, not scores.

Asset retention (compliance). Signed URLs expire (~1h). If we must retain ID/face media, download and re-store promptly in our own (FR-sovereign, PCI/GDPR-aware) storage — these are sensitive biometric/identity assets. Retention policy is a compliance decision (§12).


10. Response codes & fraud detection

ResponseCode is an int 10000–69999; the leading digits encode meaning:

Fraud flag (legacy parity): treat codes such as photocopy/screen/counterfeit/ face-mismatch/face-swap (62201, 62202, 62301, 62302, 62303, 62304, 62305, 62306, 62307, 62321, 62399, 62403) as fraudulent vs. mere technical/engagement failures — surface this distinction to the back office.


11. Testing (no separate sandbox URL)

Same base URL + test mTLS cert/credentials. Drive scenarios with magic external_applicant_id on the applicant:

Combine with POST …/notify to fire webhooks on demand. The business-app test module (deliverable) will let us request a verification and watch the result end-to-end against these.


12. Open items to confirm with Ubble / compliance

  1. Basic-auth credentialsRESOLVED (2026-06-30): a live probe with the test cert showed mTLS alone authenticates (401 without cert → 405 with cert on GET /v2/applicants). Basic client_id/client_secret are not required; left as Secret::todo().
  2. Cko-Signature exact construction — confirm body+timestamp ordering, separator, and signature encoding (hex vs base64url) against github.com/ubbleai/code-samples and a captured webhook.
  3. Signature key env token — our keys say prod/test; docs examples say production/live. Match on the literal header segment; confirm the real token Ubble sends.
  4. user_journey_id (Certified/PVID config)RESOLVED (2026-06-30): the “Green-Got” journey usj_01h13smkbjh2x48zn7v6mkp8g0 is provisioned and set in all environments. (If Ubble later issues a distinct live journey, update production().)
  5. Asset retention policy — what ID/face media must we retain, for how long, and in which storage, given PCI/GDPR + FR-sovereignty? Determines the archival use-case.
  6. Onboarding seam shape — confirm the move from synchronous verify() to start + webhook-driven completion (§3).
  7. Key rotation — the production mTLS private key was shared over chat during this work; recommend rotating it before go-live.

13. Deliverables (build plan)

  1. src/env/src/parameters/checkout.rs — params + gg-pushed secrets; registered in parameters/mod.rs. Done for the mTLS cert/key, signature public keys, and API URL (signature_key_id set). Pending Ubble: Basic client_id/client_secret (Secret::todo()) and user_journey_id (empty).
  2. src/clients/src/checkout/ — transport client (mTLS via mtls crate, Basic auth, v2 DTOs, operations, Cko-Signature verifier); pub mod checkout; in clients/src/lib.rs.
  3. Datamodel + migrationscheckout_pvid local mirror tables (applicant, verification, attempt, document, response_codes, assets) in src/db/migrations/.
  4. src/commercial_domain/checkout_pvid/ — domain crate: stores, use-cases (start / handle-webhook / refresh / archive-assets / back-office queries), status machine, and the IdentityVerificationProvider impl wired into onboarding.
  5. Back-office API#[handler(domain = "crm")] endpoints to list/inspect/manage verifications.
  6. Back-office routes/views/crm/checkout_id_verification under /crm/validations: list attempts/successes with status + link to user / onboarding / KYC review.
  7. Business-app test module — request an identity verification and observe the result end-to-end.

Sources

Customer Billing

0. Documentation Index

Customer Billing — Documentation Index

The customer_billing crate owns Green-Got’s customer-billing subledger — Green-Got billing its own customers for the services it sells them. It is the source of truth for billable events, immutable gap-free-numbered invoices, credit notes, payments, refunds, and the non-fiscal monthly statement, and it produces the accounting-source data the accounting crate maps to the GL (UC-ACC-13). These documents are the authoritative target design for the subledger.

It is distinct from the business_domain/invoicing crate (accounts receivable for business customers issuing to their clients — a different issuer, a different gap-free series). See UC-BIL-12 and 1. Overview.

The cross-crate use cases (UC-BIL-, UC-ACC-, UC-REG-*) live in ../../docs/0_use_cases.md and are the upstream source of truth every rule below cites.

Overview & model

Billing principles

Architecture

Other

1. Customer Billing Overview

Customer Billing Overview

This document defines the scope of the customer_billing crate — Green-Got’s customer-billing subledger — and draws the boundaries that separate it from the invoicing crate, from core_banking funds, from accounting, and from savings/partner products.

1. Terminology

2. Scope — what customer_billing owns

This crate owns the full customer-facing money lifecycle for services Green-Got sells:

Design rule — segment-neutral. The crate is pure data + logic with no imports of segment systems (Appendix A locked premise). Retail and B2B customers bill through the same machinery; the segment is data, never a code branch.

3. The hard contrast with the invoicing crate

This is the single most important distinction in this crate and is made explicit per UC-BIL-12.

Concerncustomer_billing (this crate)business_domain/invoicing
Who is the issuer/sellerGreen-Got itselfThe business customer
Who is the buyerThe Green-Got customer (holder)The business customer’s own client
What it billsGreen-Got’s services (subscriptions, fees)The customer’s goods/services to third parties
Gap-free seriesGreen-Got’s own series, per Green-Got legal entityThe business customer’s series, per their org
E-invoicing transmissionE-reporting / B2B transmission per 8Transmitted via plateforme_agreee (B2Brouter)
NatureA private subledgerAccounts receivable for a third party

Design rule: Both crates enforce the same French gap-free discipline (CGI art. 289), but they are different issuers with different sequences and must never share a numbering counter or be conflated (UC-BIL-12). See 4. Numbering & Immutability.

Invariant: A customer_billing invoice always has Green-Got as the seller and a Green-Got customer as the buyer. An invoice whose seller is a Green-Got business customer is an invoicing document, never a customer_billing row.

4. Boundaries — what it references but does not own

OwnsReferences (owned elsewhere)
Billable events, invoices, credit notes, payments, refunds, statementsThe banking account / IBAN / balance / safeguarded liquidity (core_banking)
Per-line recognition metadata; tax-code snapshotsSubscription lifecycle, free-month counter, proration instructions (subscriptions)
Accounting-source dataChart-of-accounts mapping; double-entry journals (accounting)
Receivable / payment / refund-liability statePayment execution, disputes, chargebacks (payments domain)
Savings / ASV / PER commission (accounting + partners)

5. Related documents

2. Data Model

Data Model

This document defines the subledger entities — BillableEvent, Invoice, CreditNote, Payment, Refund, Statement — their per-line recognition metadata, and the invariants that bind them, each mapped to its source use case. Monetary amounts are EUR integer cents throughout (UC-BIL-23).

1. Entity-relationship overview

erDiagram
    BILLABLE_EVENT ||--o| INVOICE_LINE : "billed as"
    INVOICE ||--|{ INVOICE_LINE : "has"
    INVOICE ||--o{ PAYMENT_ALLOCATION : "settled by"
    PAYMENT ||--|{ PAYMENT_ALLOCATION : "allocates"
    INVOICE ||--o{ CREDIT_NOTE : "corrected by"
    CREDIT_NOTE ||--|{ CREDIT_NOTE_LINE : "has"
    CREDIT_NOTE ||--o| REFUND : "may settle as"
    REFUND }o--o| INVOICE : "returns cash for"
    STATEMENT }o--o{ INVOICE : "aggregates"
    STATEMENT }o--o{ PAYMENT : "aggregates"
    STATEMENT }o--o{ CREDIT_NOTE : "aggregates"

    BILLABLE_EVENT {
        id bev_id PK
        string source_ref "subscription cycle / transaction"
        timestamp recognition_at "exact recognition date (UC-BIL-04)"
        i64 amount_cents
        string tax_code "dated VAT policy key (UC-ACC-08)"
        enum status "captured | billed | discarded"
    }
    INVOICE {
        id inv_id PK
        string number "gap-free per Green-Got legal entity (UC-BIL-12)"
        id legal_entity_id "Green-Got issuer"
        id customer_ref "the buyer (holder)"
        enum status "open | partially_paid | paid | written_off | void"
        i64 amount_due_cents "DERIVED: total - allocated - credited"
        timestamp issued_at
    }
    INVOICE_LINE {
        id line_id PK
        id invoice_id FK
        enum kind "subscription | one_off | discount | free_month_credit"
        i64 gross_cents
        i64 discount_cents
        i64 net_cents
        i64 vat_cents
        string tax_code
        date recognition_start
        date recognition_end
        id source_billable_event_id FK "nullable"
    }
    CREDIT_NOTE {
        id cn_id PK
        id corrects_invoice_id FK "nullable for non-invoice credits"
        string reason_code "mandatory (UC-BIL-16)"
        enum settlement "netted_next | refund_liability | applied"
        i64 amount_cents
        timestamp issued_at
    }
    PAYMENT {
        id pay_id PK
        id customer_ref
        i64 amount_cents "what was available; partial-ok (UC-BIL-24)"
        enum method "internal_transfer"
        timestamp received_at
    }
    PAYMENT_ALLOCATION {
        id alloc_id PK
        id payment_id FK
        id invoice_id FK
        i64 amount_cents "oldest-first allocation (UC-BIL-24)"
    }
    REFUND {
        id ref_id PK
        string reason_code "mandatory (UC-BIL-16)"
        string legal_basis "withdrawal | statutory | error | termination"
        i64 amount_cents
        timestamp sla_deadline "withdrawal SLA (UC-BIL-21)"
    }
    STATEMENT {
        id stm_id PK
        id customer_ref
        date period_start
        date period_end
        i64 opening_balance_cents
        i64 closing_balance_cents
        i64 amount_due_cents "combined across immutable invoices (UC-BIL-24)"
    }

2. BillableEvent

The pre-invoice fact: a chargeable economic event captured at the moment it happens, carrying its exact recognition date (UC-BIL-04).

3. Invoice and InvoiceLine

The immutable customer invoice. Every charge has one (UC-BIL-12).

Invoice status

open → partially_paid → paid, with open | partially_paid → written_off (UC-BIL-10) and open → void for never-collectible drafts. Driven by collection and dunning — see 5. Partial Payment & Dunning.

4. CreditNote

Reduces what the customer owes (UC-BIL-07/16).

5. Payment and PaymentAllocation

Settlement of receivables by internal transfer (UC-BIL-05).

6. Refund

Cash returned to the customer for the non-waivable legal / correction cases (UC-BIL-09/14/21).

7. Statement

A non-fiscal monthly aggregate of a customer’s billing activity (UC-BIL-11).

8. Accounting-source projection

Every entity above contributes to the accounting-source data exported to accounting: gross revenue, discounts/credit notes, VAT/tax-code and rate, recognition period, receivable/payment state, refund/customer-credit liability, reason code, product/module reference, and customer/legal-entity references (UC-ACC-13). This crate produces these fields; it does not map the chart of accounts or generate journals. See 9. Architecture §4.

9. Related documents

3. Billing Model — In Advance

Billing Model — In Advance

This document specifies when a customer is billed and how the fee is collected: recurring subscription fees are always billed in advance, one-off fees at the event, and collection is by internal transfer — never SEPA direct debit.

1. Recurring fees are billed in advance

Design rule — always in advance, never in arrears. A recurring subscription fee is billed in advance, at the start of the period it covers — never at period end (UC-BIL-01/02). The customer pays upfront for the upcoming period; accounting then recognises it over that period (deferred revenue / PCA, UC-ACC-01/02).

2. One-off fees are billed at the event

Design rule — captured at the moment it happens. A chargeable transaction (ATM, FX, card reissue) creates a BillableEvent at the moment it happens, recognised at the event date (UC-BIL-04, UC-ACC-03). Each fee is one invoice line with its exact recognition date; a day’s fees are never collapsed into one batched line (UC-BIL-04). Per-customer aggregation lives on the monthly statement (UC-BIL-11), not on the invoice.

Design rule — one-off fees apply even on a free offer. A €0 offer produces no recurring subscription invoice (no recurring obligation arises) and no €0 invoice at all, but a one-off fee on it (lost-card replacement, ATM withdrawal) still creates a BillableEvent and an invoice line exactly as on a paid offer (UC-BIL-13, UC-OFF-08).

3. Free-month counter

Design rule — the counter lives on the subscription. A free-month counter lives on the subscription (owned by subscriptions; this crate reads it at each cycle) (UC-BIL-17). At each billing cycle:

There are no free trials in the product; this same mechanism backs any free-first-month promotion (UC-BIL-17). Referral rewards increment this counter (UC-BIL-20).

4. Proration on upgrade (close-then-open with netting)

Design rule — full-price new line + prorata credit on the old. At an upgrade switch the old subscription is stopped and the new one opened with its own anniversary at the switch date. The invoice carries two visible lines — (1) the full price of the new subscription for its first period, and (2) a prorata credit note for the old subscription’s unused prepaid days (daily pro-rata on gross) — and nets them so the customer pays the difference (UC-BIL-08). Each line carries its own recognition period; the filiation link is recorded.

5. Collection by internal transfer (no SEPA DD)

Design rule — internal transfer only. The fee is collected by an internal transfer from the customer’s payment-account sub-ledger position (inside the safeguarded/ring-fenced Crédit Mutuel liquidity pocket) to Green-Got’s functioning/operating account at Crédit Mutuel — no SEPA direct debit (UC-BIL-05). This crate requests the transfer; core_banking executes it.

6. Savings: no customer billing

Design rule. Savings/ASV/PER/livret bill the customer nothing — no billable event, no invoice, no collection in this crate. The only economic event is partner commission, which lives in accounting (UC-BIL-15 / UC-ACC-07).

7. Related documents

4. Invoice Numbering & Immutability

Invoice Numbering & Immutability

This document specifies the gap-free numbering of Green-Got’s own customer invoices and the immutability guarantee that the partial-payment / dunning machinery depends on.

1. Green-Got is the issuer

Design rule — Green-Got’s own gap-free series. Every charge has an invoice (subscription fees and one-off fees alike), numbered sequential and gap-free per Green-Got legal entity (UC-BIL-12). Because the issuer is Green-Got itself, this is Green-Got’s own series — distinct from the invoicing crate, whose numbering belongs to the business customer issuing to their clients (a different issuer). Same gap-free discipline (FR art. 289 CGI), separate sequence (UC-BIL-12).

Invariant — never a shared counter. A customer_billing invoice number is drawn from Green-Got’s sequence keyed by (green_got_legal_entity_id[, year]). It never shares a counter with, collides with, or is conflated with an invoicing (business-customer) number (UC-BIL-12). See 1. Overview §3.

2. The French gap-free requirement

French law requires invoices issued by a legal entity to be numbered unique, chronological, and gap-free (CGI art. 289 / 286). The practical consequences for this subledger:

Design rule — numbers attach to issued invoices only. A not-yet-issued draft holds no slot. The gap-free requirement attaches to the issued invoice (UC-BIL-12). (Note: because a €0 offer produces no invoice at all, no number is consumed for it — UC-BIL-13.)

3. Issuance-time allocation (target design)

flowchart TD
    Start["Issue invoice (cycle opening or one-off fee)"] --> Begin["BEGIN transaction"]
    Begin --> Lock["Serialise the series
(advisory lock / atomic counter upsert on
green_got_legal_entity_id, year?)"] Lock --> Incr["last_number = last_number + 1
format number (per legal entity)"] Incr --> Write["Write number + lines + status=open
on the immutable invoice"] Write --> Commit{"Commit succeeds?"} Commit -->|"yes"| Done["Number consumed exactly once;
series gap-free"] Commit -->|"no / retry"| Rollback["ROLLBACK — counter unchanged,
no number consumed, no gap"] Rollback --> Begin Done -.->|"later: correct"| CN["Issue credit note (reason-coded)
— original keeps its number"]

Invariant — atomic, gap-free allocation. For a given series the allocator returns the smallest sequence number not yet issued, exactly once, inside the same transaction that writes the invoice. A rolled-back issuance leaves the counter unchanged; a committed one consumes exactly one number. No two callers receive the same number and none is skipped.

Design rule — high-water mark, never decrement. The counter only ever increases. A committed number is never released or reused, even if collection later fails or the invoice is written off (UC-BIL-10).

Design rule — gaps are surfaced, not healed. A defence-in-depth reconciliation scans each active series for any missing sequence value and raises a visible operator alert rather than silently back-filling — a real gap is a compliance defect requiring human investigation.

4. Immutability

Invariant — an issued invoice is immutable. Its lines, totals, and number are locked. It is never edited and never deleted (UC-BIL-12). All downstream activity changes related rows, never the invoice body:

Invariant — carried shortfall is never folded into a later invoice. A still-open shortfall stays on its own immutable invoice; the next cycle’s invoice is issued at full price, and the customer owes both (UC-BIL-24). The combined figure is presented only on the statement (UC-BIL-11), never by mutating an invoice. See 5. Partial Payment & Dunning.

5. Internal id vs legal number

Green-Got stores two distinct identifiers per invoice and they are never the same value:

IdentifierWhat it isWhen it existsMutable?Used for
Internal id (inv_…)Opaque, collision-proof, time-sortable keyAt creation, before any numberNeverPrimary key, FKs, event payloads, idempotency
Legal numberGap-free {prefix}-{year}-{seq} per Green-Got legal entityAt issuanceNever, once assignedThe legal invoice identifier shown to the customer

Invariant: Systems key on the internal inv_… id; the tax administration and the customer read the legal number. A missing/not-yet-assigned legal number never blocks internal references, and the internal id is never used as the legal number.

6. Related documents

5. Partial Payment & Dunning

Partial Payment & Dunning

This document specifies what happens when a charge cannot be collected in full: partial payment, the carried shortfall (oldest-first, with no minimum and incoming-fund sweep), the failed-charge dunning cadence, and the write-off-vs-credit-note decision.

1. Failed or partial charge → unpaid invoice

Design rule — an unpaid invoice, never the absence of one. A failed internal transfer (insufficient funds) produces an unpaid invoice (open → overdue), or a partially_paid one where only part was available — not the absence of an invoice (UC-BIL-06, UC-BIL-24). It drives the dunning/retry cadence (UC-SUB-14).

Design rule — take a partial payment rather than nothing. When the balance cannot cover the full fee, collection transfers what is available (e.g. €5 of a €10 fee) and allocates it to the invoice (UC-BIL-05/24). The invoice goes open → partially_paid with a remaining balance; it is not mutated (invoices are immutable, UC-BIL-12). See 4. Immutability.

Design rule — no minimum threshold. We take whatever is available, however small (owe €10, have €0.10 → take the €0.10; next period invoice again and take whatever is there) (UC-BIL-24).

2. Carried shortfall, oldest-first

Design rule — the shortfall is carried forward across immutable invoices. At the next cycle the new period’s invoice is issued as normal (e.g. €10), so the customer owes the new invoice + the still-open shortfall (e.g. €15 total). The old shortfall is never rolled into the new invoice (UC-BIL-24).

stateDiagram-v2
    [*] --> open: invoice issued (in advance)
    open --> partially_paid: partial transfer allocated (UC-BIL-24)
    open --> overdue: due date passed, unpaid (UC-BIL-06)
    partially_paid --> overdue: remainder still owed past due
    overdue --> partially_paid: later partial collection / sweep
    partially_paid --> paid: balance fully covered
    overdue --> paid: balance fully covered (sweep / retry)
    open --> paid: collected in full
    overdue --> written_off: dunning exhausted, uncollectible (UC-BIL-10)
    partially_paid --> written_off: dunning exhausted, uncollectible
    written_off --> paid: later recovery (UC-BIL-10 "recovered")
    paid --> [*]
    written_off --> [*]

3. Anti-gaming: sweep incoming funds

Design rule — seize the amount due before it can be spent. A customer who empties the account before each cycle to dodge the fee stays in debt; incoming funds are swept to the outstanding balance first — when money appears we seize the amount due (oldest-first) before it can be spent (UC-BIL-24). This is the on_incoming_funds rule in 9. Architecture.

Design rule — persistent avoidance ends in closure. Persistently uncollected balances are written off (UC-BIL-10), and persistent avoidance ends in account closure (UC-SUB-19) after write-off (UC-BIL-24).

4. Dunning cadence

Design rule — the failed-charge dunning/retry cadence. Each open balance follows the dunning/retry cadence (UC-BIL-06 / UC-SUB-14) at offsets relative to the charge date:

OffsetAction
−1 daypre-charge notice / ensure-funds reminder before the attempt
+1 dayfirst retry after a failed/partial charge
+1 weeksecond retry
+1 monththird retry
+2 monthsfinal retry; on continued failure the balance is escalated to write-off

5. Write-off vs credit note

This is the decisive classification at the end of dunning, mapped from UC-BIL-10.

SituationTreatmentAccounting
Uncollectible valid debt (customer still owes, cannot pay)Write-off — drop from active receivablesRevenue kept, loss recognised (créance irrécouvrable, UC-ACC-10)
Customer no longer owes (error / dispute / gesture)Credit note, reason-codedReverses revenue (contra-revenue, UC-ACC-09)

6. Receivables reconcile to the GL

Invariant — Σ open invoice balances = GL client-receivable balance. At any date the sum of open invoice balances equals the GL client-receivable balance (UC-ACC-11), supporting aged-balance buckets and doubtful-debt provisioning. This crate provides the open-balance source data; accounting reconciles (UC-ACC-13).

7. Related documents

6. Refunds & Reason Codes

Refunds & Reason Codes

This document specifies the refund posture (commercial “no”, legal/correction “yes”), the mandatory reason-code taxonomy that classifies every credit / refund / reward for accounting, and the online withdrawal / rétractation function.

1. Refund posture: commercial “no”, legal/correction “yes”

Design rule — no voluntary early unused-time refund, but legally required refunds are modelled. The commercial product posture is no voluntary early unused-time refund: downgrades and cancellations are normally effective at the period boundary, with perks retained until then (UC-BIL-09, UC-SUB-07/23). That is not an absolute accounting rule — the model keeps an auditable refund/payout mechanism for legally required, correction, and residual cases (UC-BIL-09).

The non-waivable refund paths that must exist:

PathTriggerReference
Error correction (irreducible)Fee charged in error or twice (bug, retry race) — returning money taken in error is legally requiredUC-BIL-09, UC-BIL-16
Payment-service statutory refundsUnauthorised / non-executed / defective payment transactions (handled in payments domain; billing records the credit-note/refund impact)UC-REG-03, UC-BIL-09
Framework-contract termination / change rightsA non-waivable rule requires pro-rata reimbursement of prepaid chargesUC-REG-01
Cooling-off / rétractation14-day distance-financial-services withdrawal (and 30-day ASV renunciation)UC-BIL-14/21
Gesture that cannot be nettedA gesture credit note on an already-paid invoice with no future invoice to offset (leaving customer) becomes a refund liabilityUC-BIL-07/09

Design rule — not a refund: own-balance payout. Returning a customer’s own balance on account closure is a payout of their own funds (UC-SUB-19), not a refund — it does not use the refund machinery (UC-BIL-09). Pending onboarding top-up return is likewise return of unactivated funds, not a subscription refund (UC-ONB-22).

Design rule — savings renunciation is commission clawback, not a customer refund. ASV/PER renunciation triggers partner commission clawback (UC-ACC-15), not a customer-invoice refund (UC-BIL-14).

2. The mandatory reason-code taxonomy

Design rule — every credit / refund / reward carries a reason code. Every credit note, customer credit, refund liability, cashback, bonus, and exceptional compensation carries a reason code; the reason code drives accounting classification and audit and is part of the billing source data exported to accounting (UC-BIL-16 / UC-ACC-06).

Reason classExampleModelled asAccounting treatment
service not renderedcard not deliveredcredit note reducing revenue; if already collected → refund liability until paidcontra-revenue (UC-ACC-09)
gesture reducing an invoiceapp-down free monthcredit note reducing revenuecontra-revenue (UC-ACC-05)
cashback / bonus / cash reward (not linked to an invoice)spend rewardcustomer credit / pending-balance liability, not an invoice credit noteliability (UC-BIL-22)
pure compensation / indemnity (not reducing a price)goodwill payoutexpense, with customer liability until settledexpense
billing error / double chargeretry racecorrection credit note + refund liability if already collectedcontra-revenue + liability

Open. The exhaustive reason-code list and the locked code→treatment binding are not yet settled — tracked as CB-2 in uncertainties.md (UC-BIL-16 / UC-ACC-06).

3. Promo, referral, cashback (reason-coded rewards)

Open. Anti-abuse and stacking (UC-BIL-19/20) and cashback rates / tax classification (UC-BIL-22) are tracked as CB-3 and CB-4 in uncertainties.md.

4. Online withdrawal / rétractation function

Design rule — supported from v0. Onboarding is entirely at distance, so withdrawal/renunciation is not an edge case — it overlaps the first paid period for essentially every paid signup (the first fee is collected on activation) and must be supported from v0 (UC-BIL-14).

Design rule — express consent to begin performance. Because the first fee is collected on activation inside the 14-day window, onboarding must capture the customer’s explicit consent to start the service during the cooling-off period. With recorded consent a later withdrawal reimburses the unused portion but the customer owes for service actually rendered up to withdrawal; without it the early charge is fully refundable (UC-BIL-14).

Design rule — online withdrawal function (Directive (EU) 2023/2673, applicable 19 June 2026). Distance financial contracts concluded online must offer an online withdrawal function (UC-BIL-21):

The function triggers the refund path (UC-BIL-14/09); for savings, commission clawback (UC-ACC-15).

Open. The exact per-product window and full-vs-pro-rata regime are owned with legal — tracked as CB-1 in uncertainties.md (UC-BIL-14).

5. Related documents

7. VAT & Tax Codes

VAT & Tax Codes

This document specifies how VAT is determined for every customer-invoice line: tax-code driven, not hard-coded by the catalogue, resolved at issuance from a dated policy and snapshotted on the line.

1. Money conventions

Design rule — EUR integer cents. All amounts are in EUR, stored as integer minor units (cents) — never floats (UC-BIL-23). Lines distinguish gross / discount / net and VAT; the customer-facing charge is the net, gross-of-VAT amount (UC-BIL-03/23). Multi-currency is out of scope (EUR only) for now.

Design rule — rounding per line at issuance, snapshotted. Rounding is computed per line at issuance and snapshotted on the line (UC-BIL-23, UC-SUB-32); it is never recomputed later. The VAT amount is stored next to its net base so the figures shown to the customer are the figures kept forever.

2. VAT is tax-code driven

Design rule — a dated tax_code per product/fee, not a hard-coded rate. VAT is tax-code driven, not hard-coded by the catalogue (UC-ACC-08). Each product/fee carries a dated tax_code; VAT is resolved at issuance from the tax_code + dated policy/rate table + territory, then snapshotted on the line (UC-ACC-08).

Invariant — resolution happens once, at issuance. The resolved rate and amount are snapshotted on the invoice line. A later change to the policy/rate table never alters an already-issued line — the invoice is immutable (UC-BIL-12, 4. Immutability).

Design rule — effective-dated policy. A tax_code’s policy is effective-dated and can change (for example if Green-Got renounces the 260 B option). Resolution uses the policy effective at the line’s issuance/recognition date (UC-ACC-08).

3. The Green-Got VAT baseline

The finance baseline mapped from UC-ACC-08:

Revenue typeTax treatmentBasis
Core banking / payment serviceswithin the art. 261 C exemption perimeterUC-ACC-08
Green-Got’s subscriptionprovisionally taxable at 20% under the art. 260 B optionUC-ACC-08
Non-banking service feestaxable by default unless their own tax code says otherwiseUC-ACC-08
Partner commissions (ASV/PER/livrets)VAT-exempt intermediation revenue (and not billed to the customer)UC-ACC-07/08, UC-BIL-15
TCA (insurance)borne by the insurer/partner, not Green-GotUC-ACC-08

Open / launch-blocking. The 260 B option is provisional, not a settled fact: it is irrevocable for 5 years, and its scope over a payment-account subscription fee (vs the 261 C exemption of the payment services themselves) must be confirmed by a tax advisor before baking it into accounting (UC-ACC-08). Tracked as CB-5 in uncertainties.md.

4. Credit notes reverse VAT

Invariant — a credit note reverses revenue and the collected VAT. A credit note reverses the corrected line’s revenue and the associated collected VAT (UC-ACC-09). The credited VAT is explicit on the credit-note line (rate + amount), mirroring the original line’s snapshot so the regularisation is auditable. See 6. Refunds & Reason Codes.

5. VAT and e-reporting scope

Design rule — VAT status drives e-reporting scope. VAT-exempt payment-service fees (art. 261 C) are out of e-reporting scope; the VAT-taxable subscription (260 B option) is in scope for B2C e-reporting (UC-BIL-18). The line’s snapshotted tax_code is what decides inclusion. See 8. E-Invoicing & E-Reporting.

6. What this crate provides to accounting

This crate exports, per line: the tax code and resolved rate, the VAT amount, the gross / discount / net split, and the recognition period — the VAT-relevant accounting-source data (UC-ACC-08/13). It does not own the VAT-account GL mapping or the VAT declaration; those live in the accounting integration layer (UC-ACC-13). The receivable/payment state it provides feeds RUBA/ACPR-input reconciliation (UC-ACC-11).

7. Related documents

8. E-Invoicing & E-Reporting

E-Invoicing & E-Reporting

This document specifies how Green-Got’s own customer invoices participate in the French e-invoicing/e-reporting reform: every invoice is built e-invoicing-compatible, with two distinct limbs — e-invoicing (B2B invoice transmission) and e-reporting (B2C transaction-data transmission).

1. Build every invoice e-invoicing-compatible

Design rule — structured, PDP-ready by construction. Every invoice Green-Got issues is built e-invoicing-compatible (structured format, Factur-X / PDP-ready) (UC-BIL-18). Green-Got is the issuer here — distinct from the invoicing crate, where the business customer is the issuer and plateforme_agreee transmits (UC-BIL-12/18). See 1. Overview §3.

2. The two limbs

The reform has two distinct limbs (UC-BIL-18):

LimbWhat it transmitsApplies toRecipient
E-invoicing (invoice transmission)the structured invoicedomestic B2B only — invoices to French VAT-taxable business customers, via a PDPthe buyer (via PDP)
E-reporting (transaction-data transmission)aggregated daily transaction data (amount invoiced + VAT collected)VAT-taxable B2C revenue, via a PDPthe tax authoritynot the customer

Invariant — e-reporting aggregates, never per-customer invoices. B2C e-reporting transmits aggregated daily amount-invoiced + VAT-collected figures, not individual consumer invoices (UC-BIL-18).

3. Timeline

Design rule — phased per the reform. Reception for all from Sept 2026; émission / e-reporting phased Sept 2026 → 2027 (UC-BIL-18). Building invoices structured from the start means no rebuild when émission/e-reporting obligations land.

4. Boundary with the invoicing / plateforme_agreee transmission stack

This crate produces Green-Got’s own structured invoice data and the e-reporting aggregates for Green-Got-issued revenue. The certified-platform transmission mechanics (B2Brouter adapter, AFNOR legal-status lifecycle, Annuaire routing) that the invoicing crate relies on are the plateforme_agreee concern for business-customer documents; whether Green-Got’s own B2B émission reuses that stack or a dedicated PDP path is a wiring decision outside this design round. What is fixed here: Green-Got is the issuer, the series is Green-Got’s own (UC-BIL-12), and the data is structured/PDP-ready by construction (UC-BIL-18).

5. Related documents

9. Architecture

Architecture

This document defines the intended DDD layering of the customer_billing crate (scaffolding-only this round), its segment-neutral posture, and the accounting-source output contract. The module layout is sketched in ../plan.md; the layer conventions follow the repo architecture skill.

1. Layered DDD structure

The crate follows the codebase’s layered-DDD convention — domain/use_cases/stores/ / infrastructure/adapters/service.rs / rules/ / workflows/:

LayerHoldsNotes
domain/Money, BillableEvent, Invoice/InvoiceLine, CreditNote, Payment/Allocation, Refund, Statement, ReasonCode, TaxCode, numberingentities + value objects + invariants; zero dependency on other layers
use_cases/issue_cycle_invoice, capture_billable_event, collect_invoice, allocate_payment, sweep_incoming_funds, issue_credit_note, write_off_invoice, issue_refund, exercise_withdrawal, build_statementorchestration only; no business rules of their own
stores/per-entity stores; numbering_store (serialised gap-free counter)*Record structs private to stores/; immutable-invoice rows, append-only payment ledger
infrastructure/adapters/core_banking internal-transfer client; notification clienttechnical concerns only — billing requests transfers, never moves funds
service.rs / rules/ / workflows/eventbus wiring; on_charge_result, on_incoming_funds rules; billing_cycle, dunning, refund_sla Temporal workflowsthe reactive + scheduled seam

Design rule — start flat, promote on growth. Use the use_cases/ and stores/ shortcuts rather than full application/ / infrastructure/ nesting until complexity warrants it (repo architecture convention).

2. Segment-neutral

Design rule — pure data + logic, no segment imports. The crate is segment-neutral: it does not import segment systems and has no retail-vs-B2B code branches (Appendix A locked premise). Retail and B2B customers bill through the same entities and use cases; the segment is data (customer reference, tax code, eligibility), never control flow. This mirrors the sibling offers, subscriptions, and accounting catalogue crates.

3. Reactive and scheduled flows

Invariant — collection is a request, never a fund movement. Workflows/use cases request an internal transfer from core_banking; this crate never owns or moves funds, preserving the UC-REG-04 / UC-REG-09 boundary.

4. The accounting-source output contract

Design rule — this crate is an accounting source, not the GL. The crate produces accounting-source data and emits it (eventbus + a typed DTO in definitions.rs); it does not own the chart-of-accounts mapping or double-entry journal generation (UC-ACC-13). The source fields it must expose (UC-ACC-13):

Invariant — billing, recognition, and payment are three distinct concerns. Billing issues the invoice, the line’s recognition period drives accounting’s revenue recognition, and payment settles the receivable — never collapsed (Appendix A locked premise, UC-BIL-05).

Design rule — partner commission is out of this crate. Savings/ASV/PER commission and its clawback (UC-ACC-07/15) are accounting’s concern; this crate emits no customer billing for savings (UC-BIL-15).

5. Related documents

Uncertainties

Customer Billing — Active Register

This is an active, classified register of the open items in the customer_billing subledger. The core billing model is documented (bill-in-advance, internal-transfer collection, partial payment + carried shortfall, gap-free immutable numbering, dunning cadence, refund posture, VAT tax-code resolution, e-invoicing limbs); this register tracks the remaining legal, product-decision, and tax items until each is closed.

Status sections. Items move through: §1 Research / decision pending (open; owned with the named owner until a grounded answer or decision is recorded), then §2 Resolved decisions (decided here; awaiting port into the named canonical doc).

Convention. “Launch-blocking = yes” means a real customer cannot be billed / refunded correctly until the item is closed. A resolved item that is launch-blocking stays launch-blocking until ported into its canonical doc.

Where these come from. Each item below carries the upstream open flag from ../../docs/0_use_cases.md (the ⚠️ OPEN markers in §5/§6).


1. Research / decision pending

CB-1 — Rétractation: per-product window & full-vs-pro-rata regime

Question: The exact withdrawal/renunciation window per product (payment-account/card vs ASV vs PER) and whether the refund on withdrawal is full or pro-rata for service rendered — including how recorded express consent to begin performance (UC-BIL-14) changes the refundable amount. Baseline: 14-day distance-financial-services (C. consommation L222-7), 30-day ASV (C. assurances L132-5-1), PER per its own regime; the online withdrawal function and ≤30-day refund SLA are fixed by UC-BIL-21.

CB-2 — Reason-code taxonomy & code→treatment binding

Question: The exhaustive reason-code list and its locked code → accounting-treatment binding (contra-revenue / customer-credit liability / expense). The five reason classes are documented (UC-BIL-16 / UC-ACC-06); the enumerated codes and their one-to-one GL mapping remain to be fixed and agreed with accounting.

CB-3 — Promo / referral anti-abuse & stacking

Question: Anti-abuse rules for promo codes (multi-redemption, self-referral) and stacking rules between codes (UC-BIL-19); and for referral, the qualifying condition, credit amounts, and per-referrer caps (UC-BIL-20). Referral rewards are regulated acquisition premiums (UC-REG-10).

CB-4 — Cashback reward rates & tax classification

Question: The cashback/reward rates and the tax/social treatment of the reward to the customer. The funding sources are resolved (business-card interchange ~2% and partner-funded; consumer debit interchange capped at 0.2% under the IFR cannot fund meaningful cashback — UC-BIL-22); the open items are rate-setting and tax classification.

CB-5 — Subscription tax_code: 260 B option validation

Question: Confirm the VAT treatment of the payment-account subscription fee. It is provisionally treated as taxable at 20% under the art. 260 B option, but this needs tax-advisor validation, not a settled fact: the 260 B option is irrevocable for 5 years, and its scope over a subscription fee (vs the 261 C exemption of the payment services themselves) must be confirmed before baking it into accounting (UC-ACC-08).


2. Resolved decisions (awaiting port into canonical docs)

None yet — this is the first design round. Items resolve here as decisions land, then port into the named canonical doc and move out of §1.


3. Closing items

An item is closed by recording the confirmed value/decision in the owning doc cited in its Port to. CB-1, CB-2, and CB-5 are launch-blocking and gate, respectively, paid-signup go-live, accounting-source export, and subscription-VAT go-live.

4. Related documents

Glossary

Commercial Domain — Glossary

Shared vocabulary across offers, onboarding, subscriptions, customer_billing, and accounting. Each crate’s docs may add crate-local terms; the load-bearing cross-crate terms live here. Definitions trace to the Use-Case Catalogue.

Catalogue & commerce

Onboarding

Subscriptions

Billing & accounting

Money conventions

All amounts in EUR, stored as integer minor units (cents), never floats. Lines distinguish gross / discount / net and VAT; rounding is per line at issuance and snapshotted. Multi-currency is out of scope. [UC-BIL-23]

Offers

0. Documentation Index

Offers — Documentation Index

The offers crate owns the “what you can buy” axis of Green-Got’s commercial model: the Offer SKU, the shared Module catalogue and Step dictionary, the offer↔module and offer↔required-step compositions, and offer eligibility / visibility / lifecycle / versioning. It is segment-neutral catalogue data + read logic; the living holder↔offer relation lives in subscriptions, executable onboarding Steps live in the onboarding crate, and the banking account lives in core_banking. These documents are the source of truth for the offers domain.

UC ids (e.g. [UC-OFF-06], [UC-ONB-02]) trace to the cross-domain spine ../../docs/0_use_cases.md.

Overview & model

Lifecycle, composition & access

Architecture

Other

To Be Created

1. Offers Overview

Offers Overview

This document fixes the scope of the offers crate, its vocabulary, and the boundary between what it owns and what it references. It is the orientation doc for the data-model, lifecycle, catalogue and eligibility documents that follow.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Vocabulary

The commercial model uses a fixed vocabulary (Appendix A locked premise — “Product” is not used):

A Subscription (a living holder↔offer relation that snapshots offer/module versions and price, [UC-SUB-32]) is not owned here — it lives in the subscriptions crate and reads this catalogue.

2. What the crate owns vs references

Design rule — segment-neutral catalogue. Per Appendix A, the catalogue crates (offers, subscriptions, customer_billing, accounting) are segment-neutral data + logic and import no segment systems. Retail vs Business is a plain segment field on the Offer; the Retail/Business gate is applied by the reader (onboarding) at the subscribable-offers route ([UC-ONB-01]), never by importing app/segment code into this crate.

OwnsReferences (owned elsewhere)
Offer SKU; Module catalogue; Step dictionary; the two composition tablesthe banking account / IBAN / ledger (core_banking)
Offer eligibility hook; restricted-offer whitelist; visibilityidentity / KYC + the executable Step behaviour (physical_person + onboarding)
Lifecycle (Draft/Active/Retired), start/end dates, versioning; is_primitive/fallback markingthe living holder↔offer relation + version snapshot (subscriptions, [UC-SUB-32])
continuous eligibility re-evaluation + fallback execution (subscriptions, [UC-OFF-04]/[UC-OFF-11])
billing, VAT computation, revenue recognition (customer_billing/accounting)

Design rule — the catalogue is read, never executed here. offers is the source the onboarding orchestrator and subscriptions read from; this crate records the catalogue and answers read queries. It does not subscribe holders, run onboarding Steps, re-evaluate eligibility at period boundaries, or bill anyone.

3. The two read surfaces

  1. Customer-facing subscribable-offers gatelist_available_offers(country, segment, …) ([UC-ONB-01]): the gate that returns only the offers a given prospect/customer can subscribe to, filtered by country, segment, visibility/whitelist ([UC-OFF-12]) and module-collision ([UC-ONB-18]). [UC-ONB-18] generalises [UC-ONB-01] for an existing customer.
  2. Back-office catalogue viewsadmin_list_offers / admin_get_offer: full content across all lifecycles, including the Module catalogue and Step dictionary libraries, for catalogue management.

Both are detailed in 6. Architecture.

4. Invariants (domain-wide)

5. Related Documents

2. Data Model

Data Model

This document defines the entities of the offers crate, the relationships between them, and the per-entity invariants. Typed ids are prefixed, time-sortable strings: OfferId (off_), ModuleId (mod_), StepId (stp_). Money is integer cents (i64).

UC ids trace to the spine ../../docs/0_use_cases.md.

As-built. In this slice these entities are realized in code, not in Postgres: the catalogue is a process-static registry in ../src/catalogue.rs with no offer / module_catalogue / offer_module / offer_required_step / offer_whitelist_entry tables. The ER diagram and per-entity invariants below are the logical model the in-code registry realises; uniqueness invariants that a DB would enforce with constraints are enforced by catalogue tests instead (see 6. Architecture §5). The Postgres mapping is the migration target in 6 §6.

erDiagram
    OFFER ||--o{ OFFER_MODULE : composes
    OFFER ||--o{ OFFER_REQUIRED_STEP : declares
    OFFER ||--o{ OFFER_WHITELIST_ENTRY : grants-access
    MODULE_CATALOGUE_ENTRY ||--o{ OFFER_MODULE : referenced-by
    STEP_DICTIONARY_ENTRY ||--o{ OFFER_REQUIRED_STEP : referenced-by

    OFFER {
        OfferId id PK
        string code "material, immutable"
        int version "material, immutable"
        string name "presentation, editable"
        CountryCode[] available_countries_of_residence "e.g. {FR}"
        LegalForm[] available_legal_forms "empty = unrestricted"
        Segment segment "Retail | Business"
        i64 price_cents "material"
        string currency "ISO 4217"
        Visibility visibility "PubliclyAccessible | Restricted"
        Lifecycle lifecycle "Draft | Active | Retired"
        timestamp start_at
        timestamp end_at "nullable"
        bool is_primitive
    }
    MODULE_CATALOGUE_ENTRY {
        ModuleId id PK
        string code UK "e.g. payment_account"
        ModuleCategory category "exclusivity class"
        int per_person_cap "uniqueness"
        ModuleId[] dependencies "required other modules"
        string tier "nullable: Essential | Premium"
        string tax_code "VAT/tax classification"
        RegulatedNature regulated_nature "banking | iobsp | ias"
    }
    STEP_DICTIONARY_ENTRY {
        StepId id PK
        string code UK "e.g. siret_lookup"
        string name
        StepKind kind
    }
    OFFER_MODULE {
        OfferId offer_id FK
        ModuleId module_id FK
    }
    OFFER_REQUIRED_STEP {
        OfferId offer_id FK
        StepId step_id FK
        int ordinal "step order"
    }
    OFFER_WHITELIST_ENTRY {
        OfferId offer_id FK
        string email "nullable, claimed"
        string phone "nullable, claimed"
    }

2. Offer

The priced commercial SKU. Composes modules ([UC-OFF-14]) and declares required steps ([UC-ONB-02]); immutable/versioned on its contractual basis ([UC-OFF-06]).

FieldTypeNotesTrace
idOfferId (off_…)Prefixed, time-sortable.
codestringStable product code (e.g. sole_trader). Material/immutable.[UC-OFF-06]
versionintAppend-only version of code. Material/immutable.[UC-OFF-06]
namestringDisplay name. Presentation/editable in place.[UC-OFF-06]
available_countries_of_residenceCountryCode[]Countries of residence the offer is available to (offers always differ by country). The subscribable-offers gate matches the prospect’s country against this list.[UC-ONB-01]
available_legal_formsLegalForm[]Business legal forms the offer is available to (e.g. AutoEntrepreneur). Empty = unrestricted by legal form (retail offers, or business offers open to every form). The gate matches the prospect’s legal form against this list when one is supplied.[UC-ONB-01]/[UC-OFF-12]
segmentSegment = Retail | BusinessPlain field; the gate is applied by the reader, not by importing segment code.[UC-ONB-01]
price_centsi64Recurring price in minor units; 0 is a valid free offer ([UC-OFF-08]). Material.[UC-OFF-06]
currencyISO 4217EUR for now.
visibilityVisibility = PubliclyAccessible | RestrictedDefault publicly accessible; restricted = whitelist-only.[UC-OFF-12]
lifecycleLifecycle = Draft | Active | RetiredSubscribable only when Active and in-window.[UC-OFF-05]
start_attimestampStart of the subscribable window.[UC-OFF-05]
end_attimestamp, nullableOptional end; after it the offer is retired.[UC-OFF-05]
is_primitiveboolA self-standing terminal fallback target (fallback = self); needs no declared fallback.[UC-OFF-13]
group_codestringVariant group the offer belongs to: siblings are the same product at different price/perk points (e.g. fr_sole_trader + fr_sole_trader_premium share group fr_sole_trader).[UC-OFF-15]

Invariant — siblings share an identical onboarding Step set. All offers with the same group_code MUST declare the same ordered required_step_codes (enforced by a catalogue assertion/test). This is what lets the confirm_offer Step switch the onboarding between siblings without invalidating the session’s snapshotted required_steps or re-asking captured data ([UC-OFF-15]/[UC-ONB-34]).

Design rule — material vs presentation. code, version, price_cents, the composed module set, and contractual entitlements are the contractual basis: append-only, never edited in place; a change is a new (code, version) row ([UC-OFF-06]). name and other copy/presentation fields are editable in place and are not part of the subscription snapshot ([UC-SUB-32]). See 3. Offer Lifecycle and Versioning.

Invariant — (code, version) is unique and immutable. Each (code, version) pair exists once and its contractual-basis fields never mutate; this guarantees every invoice/subscription links to a frozen, fixed-price offer ([UC-OFF-06]/[UC-SUB-32]).

Invariant — eligibility-bearing offers declare a fallback. Any non-primitive offer that may be force-migrated must resolve to an active fallback ([UC-OFF-11]/[UC-OFF-13]); a primitive (is_primitive = true) is its own terminal fallback. See 5. Eligibility, Visibility and Fallback.

Not implemented in this slice. The fallback declaration and its coverage assertion are not built; non-primitive offers in the in-code catalogue currently have no fallback target. This must land together with the subscriptions force-migration path before any non-primitive offer can be force-migrated. See 5 §4.

3. ModuleCatalogueEntry

The shared Bibliothèque de modules — each module defined once, composed into offers ([UC-OFF-14]). The spec carries the rules; exclusions/dependencies are derived from it, not from a separate matrix.

FieldTypeNotesTrace
idModuleId (mod_…)Prefixed, time-sortable.
codestring, uniquee.g. payment_account, shared_account, card, livret, assurance_vie, per.[UC-OFF-14]
categoryModuleCategoryExclusivity class (e.g. current_account), used for collision.[UC-OFF-14]
per_person_capintPer-person uniqueness (e.g. one current_account).[UC-SUB-11]
dependenciesModuleId[]Required other modules (livret/ASV/PER require a current_account).[UC-SUB-11]
tierstring, nullableEssential | Premium where applicable; drives entitlements. The account has no tier (Appendix A).[UC-OFF-14]
tax_codestringVAT/tax classification (effective-dated policy lives in accounting).[UC-ACC-08]
regulated_natureRegulatedNaturebanking | iobsp | ias — routes conduct rules.[UC-REG-06]

Design rule — no separate exclusion matrix. Collision and dependency are derived from category + per_person_cap + dependencies. The subscribable-offers exclusion ([UC-ONB-18]) compares an offer’s modules’ categories/caps against the customer’s held modules; a category already held one-per-person excludes the offer (the upgrade is offered instead).

Invariant — code is unique in the catalogue. The same Module is the same catalogue entry everywhere; an offer never invents an inline module ([UC-OFF-14]).

4. StepDictionaryEntry

The shared registry of reusable onboarding Steps ([UC-ONB-02]). This crate holds definitions only; the executable behaviour lives in the onboarding crate, keyed by the same code.

FieldTypeNotesTrace
idStepId (stp_…)Prefixed, time-sortable.
codestring, uniquee.g. personal_info, id_doc, mobile_otp, proof_of_address, siret_lookup, kyb_docs, recap.[UC-ONB-02]
namestringHuman label.
kindStepKindCoarse classification of the requirement.[UC-ONB-02]

Design rule — definition here, behaviour in onboarding. The Step dictionary is the “what you must do” axis; the applies_to(offer, bag)-gated executable flow lives in the onboarding crate ([UC-ONB-13]). Both sides share one code. B2B and retail Steps share one dictionary.

Invariant — code is unique in the dictionary. A Step (e.g. id_doc) is the same registry entry wherever it appears ([UC-ONB-02]).

5. OfferModule (composition)

Join row: which modules an offer composes ([UC-OFF-14]).

FieldTypeNotes
offer_idOfferIdThe composing offer.
module_idModuleIdA catalogue entry it includes.

Invariant — (offer_id, module_id) unique. A module appears at most once in an offer’s composition; the composition is part of the offer’s material basis ([UC-OFF-06]).

6. OfferRequiredStep (offer drives required steps)

Join row with ordering: the offer’s declared subset of the Step dictionary ([UC-ONB-02]).

FieldTypeNotes
offer_idOfferIdThe offer.
step_idStepIdA dictionary entry it requires.
ordinalintOrder of the step in the offer’s onboarding.

Design rule — the offer drives the onboarding. The backend creates an onboarding session from the offer’s declared required Steps; the offer never invents inline steps ([UC-ONB-02]).

Invariant — (offer_id, step_id) unique and ordinal total-orders the steps within an offer.

7. OfferWhitelistEntry (visibility hook)

Minimal eligibility/whitelist hook for restricted offers ([UC-OFF-12]).

FieldTypeNotes
offer_idOfferIdThe restricted offer this entry grants access to.
emailstring, nullableA claimed (not necessarily pre-verified) email.
phonestring, nullableA claimed phone.

Design rule — per-offer, claimed-identity whitelist. Granularity is per offer; the match may be on a claimed email/phone because abuse is caught at manual account validation ([UC-ONB-05]); managed in the back office by customer-care officers ([UC-OFF-12]). See 5. Eligibility, Visibility and Fallback §3.

Not implemented in this slice. OfferWhitelistEntry and the whitelist match are not built. Until they land, a Restricted offer is unreachable for everyone, so the in-code catalogue is guarded against shipping one: the no_restricted_offers_until_whitelist_exists catalogue test fails at CI/boot on any Visibility::Restricted offer.

8. Related Documents

3. Offer Lifecycle and Versioning

Offer Lifecycle and Versioning

This document defines the Offer lifecycle (Draft → Active → Retired) with its start/end dates and retirement semantics ([UC-OFF-05]), and the immutable (code, version) versioning model — the material-vs-editable split ([UC-OFF-06]).

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Lifecycle state machine ([UC-OFF-05])

Every offer has a start date and an optional end date. It is subscribable only within [start_at, end_at] and only while Active. After the end date it is Retired and no longer subscribable. Retirement does not close existing subscriptions — they are grandfathered and migrate only on an explicit event ([UC-SUB-17], [UC-OFF-11]).

stateDiagram-v2
    [*] --> Draft: create (catalogue authoring, not yet sellable)
    Draft --> Active: publish (within [start_at, end_at]; required steps + modules set)
    Draft --> [*]: discard draft (a Draft offer never sold may be deleted)

    Active --> Retired: end_at reached, or explicit retire
    Retired --> [*]: terminal for new sales

    note right of Retired
        Retired = not subscribable to NEW customers.
        Existing subscriptions are grandfathered
        ([UC-SUB-17]); they migrate only on an
        explicit event, to an ACTIVE fallback
        ([UC-OFF-11]/[UC-OFF-13]).
    end note

Key characteristics:

Invariant — retirement never strands a customer. A retired offer must still resolve to an active fallback for any forced migration ([UC-OFF-05]/[UC-OFF-13]); fallback integrity is a continuously-verified property — see 5. Eligibility, Visibility and Fallback.

Invariant — grandfathering. Existing subscriptions on a retired offer do not close; they reference their frozen version snapshot ([UC-SUB-32]) until an explicit migration event.

2. Versioning and immutability ([UC-OFF-06])

An offer has two kinds of field:

Design rule — the classification test. Does the change alter what the customer gets or pays (price, composition, a contractual entitlement)?

⚠️ Borderline cases (a real entitlement vs merely its description) are classified per this test; when in doubt, treat as material ([UC-OFF-06]).

Invariant — append-only contractual basis. A (code, version) row’s material fields never mutate. Every invoice links to an immutable, fixed-price offer, and every live subscription references a frozen version ([UC-OFF-06]/[UC-SUB-32]).

3. Cross-generation independence ([UC-OFF-10])

Valentine 2027 and Valentine 2028 are independent immutable rows. Because pricing is intrinsic and snapshotted (no runtime discount stacking — [UC-OFF-03]), generations cannot conflict. Continuing-eligibility transitions ([UC-OFF-11]) also avoid conflict because each offer resolves to its own declared fallback, never to another generation’s offer. This is a property to prove (the fallback graph never crosses generations), not merely assert.

Design rule — promos are intrinsic, not stackable. A time-phased promo price (e.g. €10/mo for 12 months, then €13) is part of the offer’s price schedule ([UC-OFF-03]); on reversion there is no new subscription. Customer-specific discounts (promo codes, parrainage, comp) are a different thing — they live on the subscription and apply at the invoicing layer ([UC-SUB-29]/[UC-BIL-19]), never on the offer.

4. Related Documents

4. Module and Step Catalogue

Module and Step Catalogue

This document defines the two shared dictionaries the offer composes from: the Module catalogue (the “what you get” axis, [UC-OFF-14]) and the Step dictionary (the “what you must do” axis, [UC-ONB-02]) — how an offer composes/declares from each, and the derived collision rules that drive the subscribable-offers exclusion ([UC-ONB-18]).

UC ids trace to the spine ../../docs/0_use_cases.md.

1. The Module catalogue ([UC-OFF-14])

There is a single catalogue (dictionary) of reusable Modules — the Bibliothèque de modules — each defined once: payment_account (tier Essential/Premium), shared_account (account + participant capability), card (sub-module: physical/virtual, Standard/Premium), livret, assurance_vie, per. Every offer composes a subset; it never invents inline modules. The same Module is the same catalogue entry everywhere, and a subscription freezes the module version it references ([UC-SUB-32]).

Each ModuleCatalogueEntry spec carries the rules — there is no separate matrix; exclusions/dependencies are derived from the specs (fields in 2. Data Model §3):

Design rule — savings are never priced bundle modules. livret/assurance_vie/per are intermediated, free, commission-funded ([UC-SUB-15]); holding one is an entry-eligibility discount on a paid offer ([UC-OFF-04]), not a priced bundle line ([UC-OFF-02]). The savings product itself stays free.

Invariant — one catalogue, referenced by id. An offer’s composition is a set of OfferModule rows pointing at catalogue entries; a module is defined once and never inlined ([UC-OFF-14]).

2. Composition: Offer → Modules

An offer’s OfferModule rows are part of its material/immutable basis ([UC-OFF-06]) — changing the composed set is a new (code, version). Offer shapes:

3. The Step dictionary ([UC-ONB-02])

The mirror of the Module catalogue for the onboarding-requirement axis. There is a single dictionary (registry) of reusable onboarding Steps — e.g. contact_details (email+phone; first on the unauthenticated path, seeds the prospect session, [UC-ONB-31]), personal_info, id_doc/PVID, mobile_otp, proof_of_address, funds_origin, topup, parental_consent, siret_lookup, kyb_docs, recap — each defined once and reused across offers. B2B and retail Steps share one dictionary.

Design rule — definitions here, behaviour in onboarding. This crate stores the Step definition (StepDictionaryEntry: code, name, kind). The executable behaviour — the flow, the applies_to(offer, bag) gate, the writes into core_banking/physical_person/organisation — lives in the onboarding crate ([UC-ONB-13]), keyed by the same code. The same Step (e.g. id_doc) is the same registry entry wherever it appears.

4. Declaration: Offer → required Steps

Every offer declares the subset of Steps it requires as ordered OfferRequiredStep rows (step_id, ordinal); it never invents inline steps. The front end requests onboarding creation for a chosen offer, but only the backend writes: it creates the onboarding session (with a DataBag) from the offer’s declared required Steps ([UC-ONB-02]). Adding a marketing capability ([UC-ONB-24]) is configuring an offer’s Step subset, not building a flow.

Invariant — required steps are a subset of the dictionary, totally ordered per offer. Each OfferRequiredStep references an existing dictionary entry; ordinal is unique within an offer.

Back-office reverse read — offers depending on a step. The offer_required_step join is read in both directions. Forward (offer → its ordered steps) drives onboarding creation; the reverse (step → the offers that require it) backs the back-office “onboarding steps” drawer, which shows a step’s details alongside the commercial offers that depend on it. The reverse read is offers::list_offers_for_step (store offer_store::list_for_step), exposed to the back-office app as POST /back_office/crm/list_offers_for_step { stepId } -> Offer[]. As with every back-office catalogue read, the API handler calls the use case, which calls the store — the API never queries the database directly.

5. Derived collision and the subscribable-offers exclusion ([UC-ONB-18])

The subscribable-offers exclusion falls out of the Module specs — there is no standalone exclusion matrix:

flowchart TD
    A[list_available_offers for existing customer] --> B{For each candidate offer}
    B --> C[Collect candidate's module categories + per-person caps]
    C --> D{Customer already holds a module of that
category at its per-person cap?} D -- yes, one-per-person --> E[Exclude offer; offer the UPGRADE instead
on the existing subscription UC-SUB-12] D -- no / genuinely different product --> F[Keep offer eligible
shared, livret, ASV remain offered] E --> G[Also apply visibility/whitelist UC-OFF-12] F --> G

Design rule — collision is derived from category + cap. Where a module is one-per-person ([UC-SUB-11]) — e.g. the payment-account / current-account module — a second offer carrying that category is not offered; the upgrade (an offer change on the existing subscription, [UC-SUB-12]) is offered instead. Genuinely different products (shared, livret, ASV) remain offered.

Invariant — no separate exclusion matrix. The gate compares an offer’s modules’ categories/caps against the customer’s held modules; exclusion is computed, never maintained as a table ([UC-ONB-18]).

6. Related Documents

5. Eligibility, Visibility and Fallback

Eligibility, Visibility and Fallback

This document defines the three access/integrity concerns the offer carries: continuous eligibility ([UC-OFF-04]), visibility and the restricted-offer whitelist ([UC-OFF-12]), and fallback integrity — the per-module standard base offer ([UC-OFF-07]) and the declared-fallback requirement that keeps a force-migration from stranding a customer ([UC-OFF-11]/[UC-OFF-13]).

Boundary note. offers owns the declarations here: the eligibility rule, the visibility flag, the whitelist, the fallback target and is_primitive. The continuous re-evaluation at each period boundary and the execution of the fallback offer change live in the subscriptions crate ([UC-OFF-04]/[UC-OFF-11]). This crate stores what to check and where to fall back; it does not run the check.

UC ids trace to the spine ../../docs/0_use_cases.md.

1. Continuous eligibility ([UC-OFF-04])

All commercial eligibility conditions are continuous. They are evaluated at subscribe/change time and re-evaluated at every subscription period (billing cycle). If a condition no longer holds at a period boundary, the subscription transitions to its declared fallback ([UC-OFF-11]) from the next period — with advance notice, perks kept to the boundary, and no commercial refund ([UC-SUB-07]). Checks happen at period boundaries, never instantaneously, so incidental intra-period state changes never disrupt a live subscription.

Design rule — conditional discounts live inside a bundle. A conditional discount is never a standalone read-once gate; it lives inside a bundle ([UC-OFF-02]). Example: a bundle whose shared account is 50% off because the holder also holds the individual payment account is re-checked each period; if the individual account is gone, the bundle falls back (the shared account reverts to its standard unit offer, [UC-SUB-05]) from the next period.

Design rule — deterministic vs arbitrary boundaries. Deterministic boundaries (age, a fixed date) are the sub-case that can be scheduled and notified in advance ([UC-SUB-20]); arbitrary conditions (“holds X”, “balance ≥ Y”) are caught at the next period boundary. Both resolve through the same declared-fallback mechanism ([UC-OFF-11]). Prefer conditions evaluated over a sustained window, or accept the gaming risk knowingly — an instant-state gate read at one boundary can be gamed across the period.

Boundary — this is commercial eligibility only. Regulatory/compliance gates (sanctions, KYC/KYB freshness, PEP/adverse-media, tax/regulatory residence, legal capacity, licence perimeter, partner eligibility) are a separate, continuously/periodically rechecked regime under [UC-REG-02], not modelled here.

Invariant — every conditional offer declares a fallback. A continuous eligibility condition is meaningless without a clean target to transition to; see §4.

2. The minimal eligibility hook

offers carries a minimal eligibility declaration on the offer (the condition + its boundary + the fallback offer + the timing), enough for the subscriptions engine to re-evaluate and transition. The structured rule language and the period-boundary scheduler are a subscriptions concern; this crate stores the declaration so the catalogue is self-describing.

3. Visibility and the restricted-offer whitelist ([UC-OFF-12])

Every offer carries a visibility flag:

A paid closed-beta / invite-only offer is the canonical restricted offer — restriction is about visibility, independent of price. Employee/ambassador free is a subscription modifier ([UC-ONB-09]), not a restricted price-0 offer.

Design rule — per-offer, claimed-identity, back-office managed. Whitelist granularity is per offer; it is managed in the back office by customer-care officers. The match may be on a claimed (not pre-verified) email/phone — abuse is caught because every account is manually validated by a compliance officer ([UC-ONB-05]): an impersonator is caught fast at validation. No hard verified-identity gate is required on the whitelist itself.

Design rule — restricted-offer surfacing rides the generic code mechanism. The restricted-offer whitelist is a specific instance of the loose campaign-code/deeplink mechanism ([UC-ONB-01]/ [UC-ONB-24]) that launches a specific onboarding and/or surfaces specific offers; the event-card QR ([UC-ONB-19]) is another instance.

Invariant — restricted offers are invisible by default. A Restricted offer is excluded from list_available_offers unless the caller’s claimed email/phone matches an OfferWhitelistEntry for that offer ([UC-OFF-12]).

Not implemented in this slice. The OfferWhitelistEntry whitelist and the whitelist-match at the subscribable-offers gate above are not built yet. With no whitelist to surface them, a Restricted offer is excluded from every start/list gate and is therefore unreachable for everyone. To stop that footgun shipping silently, the in-code catalogue is guarded at CI/boot: the no_restricted_offers_until_whitelist_exists catalogue test fails if any offer declares Visibility::Restricted until the whitelist feature lands.

4. Fallback integrity ([UC-OFF-13])

Offer switches (age-out, condition-no-longer-met, retirement, bundle dismantle) all need a target to fall back to, so an offer that can be force-migrated must resolve to an active fallback at all times.

stateDiagram-v2
    [*] --> NonPrimitive: offer may be force-migrated
    [*] --> Primitive: is_primitive = true (fallback = self)

    NonPrimitive --> ResolvesActive: declared fallback is an Active, in-window offer
    NonPrimitive --> BrokenChain: fallback retired with no replacement

    ResolvesActive --> [*]: healthy — transition is clean
    Primitive --> [*]: terminal fallback target — needs no fallback

    BrokenChain --> Alarm: operational alarm → customer support resolves in BO (UC-BO-08)
    Alarm --> ResolvesActive: fallback repointed to an active offer

Design rule — per-module standard base offer ([UC-OFF-07]). Every module maps to a standard unit offer used as the fallback target on bundle dismantle ([UC-SUB-05]). The fallback is derived per module, not stored per offer.

Design rule — primitives are terminal. A primitive offer (e.g. the standard Essential payment account) is self-standing and needs no fallback — it is the terminal fallback target. A primitive is marked by fallback = self; an explicit is_primitive flag is also carried for clarity ([UC-OFF-13]).

Invariant — continuous fallback-coverage assertion. The system continuously verifies (startup assertion + periodic check) that every non-primitive offer that may require a forced migration resolves to an active fallback — mirroring the onboarding engine’s registry-coverage assertion. A broken chain (fallback retired with no replacement) is an operational alarm that alerts customer support, who resolve it in the back office ([UC-BO-08]) before it can strand a customer.

Not implemented in this slice. Neither the per-offer fallback declaration nor the fallback-coverage assertion is built. Non-primitive offers in the current in-code catalogue therefore carry no fallback target — this is safe only because nothing force-migrates them yet. The declaration + coverage assertion must land together with the subscriptions force-migration path, before any non-primitive offer can be force-migrated.

Design rule — fallback proves a target, not a free price increase. If falling back would increase the customer’s price or materially change the framework contract, the transition must go through [UC-REG-01] notice/consent/rejection rules. Fallback integrity proves a target exists; it does not authorise a silent price increase. If the fallback cannot be applied with data already held (e.g. minor→adult needs full adult KYC and a new payment account), the transition is a triggered re-onboarding, not a silent migration ([UC-OFF-11]/[UC-SUB-20]).

5. Related Documents

6. Architecture

Architecture

This document defines the crate’s DDD layering, the catalogue-access map, and the read-API surface (list_available_offers, admin_*). The behavioural specs are in docs 1–5; this is how they are arranged in code.

UC ids trace to the spine ../../docs/0_use_cases.md. The build-oriented sketch is in ../plan.md.

As-built — the catalogue is defined in code, not in Postgres. In this slice the offer catalogue, the module library, the step dictionary and every composition are a process-static registry in catalogue.rs (mirroring the onboarding STEP_REGISTRY). There is no offer / module_catalogue / offer_module / offer_required_step / offer_whitelist_entry table and no seed — the catalogue is small, engineering-owned config that stays fully typed and performant in code. The DB shape the entities describe is retained as a target appendix (§6) for if/when the catalogue moves to Postgres. The entity/invariant model in 2. Data Model is the shape the in-code registry realises.

1. DDD layers

offers is segment-neutral catalogue data + read logic: no external adapters, no Temporal, no webhooks, and it emits no events of its own (the subscription snapshot and the onboarding session are written by the reader crates). A shallow DDD layout per the architecture skill:

src/commercial_domain/offers/src/
├── lib.rs            # pub mod declarations + re-exports
├── catalogue.rs      # the process-static OFFERS / MODULES registry (no DB)
├── domain/           # entities, value objects, ids — zero dependency on other layers
├── use_cases/        # the read use cases (shortcut for application/use_cases/)
└── stores/           # pure in-memory catalogue-access fns (shortcut for infrastructure/stores/)

2. Catalogue-access map

The stores/ functions are pure reads over the in-code catalogue (no tables):

ModuleBacked byResponsibility
offer_storecatalogue::OFFERSFind/list offers; sellability + start gate; (country, segment, legal_form) availability; variant-group siblings; back-office listing.
catalogue_storecatalogue::MODULES / OFFERSThe shared module library, per-offer module composition, and per-offer ordered required steps.

Design rule — (code, version) immutability is inherent to the in-code catalogue. The contractual-basis fields are compile-time constants edited only by an engineering change, so a change is a new (code, version) entry in OFFERS rather than an in-place mutation; the catalogue_identifiers_are_unique test backs (code, version) uniqueness in lieu of a DB constraint ([UC-OFF-06]).

3. Read API surface

list_available_offers(country, segment, identity?, held_module_categories) -> Vec<Offer>

The subscribable-offers gate ([UC-ONB-01], generalised by [UC-ONB-18]). Pure read:

flowchart TD
    A[list_available_offers] --> B[offer_store: Active + in-window for country+segment]
    B --> C{Visibility}
    C -- PubliclyAccessible --> E[candidate]
    C -- Restricted --> D{identity email/phone whitelisted for this offer?}
    D -- yes --> E
    D -- no --> X[drop]
    E --> F{Collision: any module category held one-per-person? UC-ONB-18}
    F -- yes --> Y[drop; upgrade offered elsewhere by subscriptions]
    F -- no --> G[return]

admin_list_offers() -> … / admin_get_offer(OfferId) -> …

Back-office views: offers across all lifecycles (Draft/Active/Retired) with full content, including the Module catalogue and Step dictionary libraries and the offer’s composition + required-step declarations, for catalogue management.

Design rule — routes call use cases. business_api / back-office routes contain no business logic; they call these use cases ([architecture skill]).

4. Events

offers publishes no domain events. The catalogue is read by:

5. Catalogue definition (no seed)

Because the catalogue lives in code (§1), there is no database seed. The catalogue is authored directly in catalogue.rs as the OFFERS and MODULES statics, and its integrity is enforced by catalogue tests rather than by seed validity + DB constraints:

Invariant — every catalogue offer is self-describing and resolvable. It composes only existing module-library entries and declares only registered step codes; the publicly-accessible, Active, in-window offers are the ones list_available_offers(country, segment, legal_form?) returns.

6. Appendix — target Postgres shape (not built)

If/when the catalogue outgrows in-code config, the entities in 2. Data Model map to this Postgres shape. None of these tables, stores or seeds exist today — this is the migration target, not the current architecture:

StoreTableResponsibility
offer_storeofferCRUD on offer rows; query Active + in-window by (country, segment).
module_catalogue_storemodule_catalogue_entryThe shared module library.
step_dictionary_storestep_dictionary_entryThe shared step dictionary (definitions only).
offer_module_storeoffer_moduleOffer→module composition.
offer_required_step_storeoffer_required_stepOffer→ordered-required-step declaration.
whitelist_storeoffer_whitelist_entryRestricted-offer access by claimed email/phone.

In that shape, (code, version) immutability is enforced by a unique constraint + write-once columns at the store boundary; the module/step libraries and the whitelist become seeded rows; and the catalogue invariant tests above become DB constraints (unique keys) plus the retained boot/coverage assertions. Moving to Postgres is also the natural point to build the restricted-offer whitelist and the fallback declaration + coverage assertion the in-code slice defers.

7. Related Documents

Uncertainties

Offers — Active Register

This is an active, classified register of the open items in the offers domain. The offer model is settled and documented (the SKU + (code, version) immutability, the shared Module catalogue and Step dictionary, the two composition tables, lifecycle/retirement, visibility/whitelist, and declared-fallback integrity); this register tracks the remaining legal, product-decision and staging-wire items until each is closed.

Status sections. Items move through: §1 Research pending (delegated to Claude to investigate, then close), §2 Resolved decisions (decided in the spine; awaiting/confirmed port into the named canonical doc), and §3 Closing items (how an item is closed).

Convention. “Launch-blocking = yes” means a real customer cannot be correctly offered/onboarded or billed until the item is closed. A resolved item that is launch-blocking stays launch-blocking until ported into its canonical doc.

Where eligibility execution and tax computation live. The continuous re-evaluation engine and fallback execution belong to subscriptions; VAT/tax computation belongs to accounting. This register holds only items genuinely owned by the offers catalogue (the declarations: tax_code values, eligibility windows, whitelist semantics).

These items are rolled up from Appendix B of the spine ../../docs/0_use_cases.md.


1. Research pending (delegated to Claude)

OFF-1 — Subscription tax code under the art. 260 B option (and the module/fee tax-code catalogue)

Question: The exact tax_code values carried on ModuleCatalogueEntry and the effective-dated policy behind them — specifically the Green-Got subscription’s classification as taxable at 20% under the CGI art. 260 B option (vs the art. 261 C exemption perimeter for core payment services), and the product/fee tax-code catalogue this references. Needs tax-advisor validation before the values are pinned; the catalogue stores the code, accounting computes from it.

OFF-2 — Eligibility validity windows and the sustained-window gaming choice

Question: For each continuous eligibility condition ([UC-OFF-04]), the per-data-point validity window (how long a value counts as “held”) and whether a condition is read as an instant state at the period boundary or over a sustained window. The spine flags the gaming risk of an instant-state gate (“balance ≥ Y”) read once per period and prefers a sustained window or a knowing acceptance of the risk; the per-data-point windows (ONB-10) are still open.

OFF-3 — Restricted-offer whitelist edge cases

Question: The precise semantics of the claimed-identity whitelist ([UC-OFF-12]) at the edges: email/phone normalisation (casing, + aliases, E.164 phone form) for matching; behaviour when an identity is whitelisted for an offer that is Retired or out-of-window; de-duplication across multiple entries; and the interaction with the generic campaign-code path ([UC-ONB-24]) that may attach a whitelist entry. The model (per-offer, BO-managed, claimed identity, abuse caught at manual validation) is decided; the matching/normalisation edge rules are not.


2. Resolved decisions (ported into canonical docs)

OFF-4 — Primitive marked by fallback = self + explicit is_primitive

Decision. A primitive offer (e.g. the standard Essential payment account) is its own terminal fallback (fallback = self); an explicit is_primitive flag is also carried for clarity. Only non-primitive force-migratable offers must resolve to an active fallback, verified continuously; a broken chain alerts customer support, who resolve it in the back office.

OFF-5 — Visibility is independent of price; “free” is a subscription modifier

Decision. Restriction is about visibility, independent of price; a paid closed-beta / invite-only offer is the canonical restricted offer. Employee/ambassador free is a subscription modifier ([UC-ONB-09]), not a restricted price-0 offer. There is therefore no price-0 “employee offer” row in the catalogue.

OFF-6 — Collision derived from module category + cap (no exclusion matrix)

Decision. The subscribable-offers exclusion ([UC-ONB-18]) is derived from each module’s category + per_person_cap; there is no separate exclusion matrix. A category already held one-per-person excludes the offer and surfaces the upgrade instead.


3. Closing items

An item is closed by recording the confirmed value/decision in the owning offers doc; for the tax-code item (OFF-1), by additionally pinning the effective-dated tax_code values against the accounting policy table once the tax advisor confirms the art. 260 B option treatment. None of the three open items is launch-blocking for the offers crate at the sole-trader launch (that offer is publicly accessible, unconditional, FR/Business), but OFF-1 is on the critical path for correct billing and is owned jointly with accounting/customer_billing.

4. Related Documents

Onboarding

0. Documentation Index

Onboarding — Documentation Index

The onboarding crate is the one consolidated, cross-segment onboarding orchestrator ([UC-ONB-13]): it drives a person from a chosen Offer to a validated, provisioned customer. Retail and business flows share one crate, one Step dictionary, and one resolver; segment differences are expressed by which Steps an offer requires and by each Step’s applies_to predicate. These documents are the source of truth for the onboarding domain.

The crate was moved from business_domain/onboarding and generalized: the engine unit Module is now a Step, and the required-data set is offer-driven (snapshotted offer_required_step rows owned by offers) instead of a hardcoded Product enum. See 1. Overview.

The catalogue-wide use cases ([UC-ONB-], [UC-MIG-], [UC-BO-*]) live in ../../docs/0_use_cases.md; every design rule below cites the use case it implements.

Engine & model

Lifecycle & provisioning

Integrations & data

Client integration

Risk & compliance signals

Open items

1. Onboarding Overview

Onboarding Overview

This document defines what the onboarding crate is, what moved into it and was generalized when it left business_domain, and the boundaries between it and the domains it orchestrates.

1. The orchestrator role

Onboarding is the single cross-segment orchestrator that takes a person from “I want this offer” to a validated, provisioned customer. It is the place where the segments meet: retail and business onboardings run through this one crate ([UC-ONB-13], Appendix A locked premise). There is no per-segment onboarding engine, and no generic engine with the Steps extracted elsewhere.

It has two orthogonal mechanisms:

  1. Data-driven content — a resolver decides what to ask next by subtracting the DataBag from the offer’s required Steps and topologically ordering the gap ([UC-ONB-03]). See 2. Engine.
  2. An explicit status lifecycle — the session moves Draft/InProgress → Submitted → UnderReview → Validated | ChangesRequested (plus Abandoned/Expired). See 4. Lifecycle.

Design rule — the backend owns the truth; the FE only renders the named Step. The front end authenticates, asks the backend for state, and renders the module for the Step the backend names. The FE never decides the next Step and never persists onboarding state itself ([UC-ONB-02], [UC-ONB-03]).

Design rule — at most one in-flight onboarding per owner. An owner cannot have two onboardings running concurrently; “your ongoing onboarding at Step X” is always singular ([UC-ONB-03]). The owner is an authenticated user or an anonymous prospect token (§1.1), so the rule is per owner — enforced by a partial unique index on user_id and a parallel one on prospect_token, both over non-terminal statuses (see 7. Data Model). Subscribing to a second offer is therefore sequential, not concurrent ([UC-J-03]).

1.1 The onboarding owner — anonymous prospect or authenticated user

Design rule — a session is owned by exactly one identity, fixed at creation. Every session belongs to one of two owner kinds ([UC-ONB-28]):

Design rule — authorization matches the caller to the owner (generalised IDOR rule). Every read / resume / submit / edit / submit-for-review is authorised by matching the caller’s identity — the prospect-token cookie for anonymous, user_id for authenticated — to the session’s owner; a mismatch is treated as not found (no existence disclosure) ([UC-ONB-28]).

Design rule — resume differs by owner kind. An authenticated session resumes across devices (server-side, keyed to the user, [UC-ONB-17]); an anonymous session resumes only while its prospect-token cookie is held — losing it abandons the session ([UC-ONB-15]) — unless the prospect authenticates and claims it ([UC-ONB-30]).

Design rule — the engine is owner-agnostic. The same Steps, registry, resolver, and validation serve both owner kinds; only the identity binding and pre-fill differ ([UC-ONB-28]). An anonymous onboarding becomes a real customer only at validation, where the Provisioner creates the user and promotes the owner from the prospect token to the new user_id ([UC-ONB-29], see 5. Validation).

2. What moved and what was generalized

The crate is the former business_domain/onboarding scaffolding moved to commercial_domain/onboarding ([UC-ONB-13]) and generalized from a B2B-only shape to a cross-segment one.

ConcernOld (B2B-only)New (generalized)
Engine unitModuleStep — same trait shape (id/provides/requires/applies_to/on_complete)
Required-data sourcehardcoded Product::required_data_points()offer-driven: the offer’s offer_required_step rows ([UC-ONB-02])
Per-session requirement setderived live from the Product enumsnapshotted into the session at create; immutable for the session’s life ([UC-ONB-03]/[UC-ONB-23])
Step libraryB2B Steps onlyone shared dictionary for all segments, gated by applies_to(offer, bag) ([UC-ONB-13])
Changing pathproduct_locked + partial reseta different offer is a fresh onboarding; offer/Step-set change mid-flight ⇒ archive ([UC-ONB-23])
End statehand-off to organisation KYB workflowvalidation freezes the session as Validated (the immutable provisioning record); real cross-domain provisioning deferred ([UC-ONB-05])

Design rule — offers drive Steps, not a Product enum. The required Step set for an onboarding is whatever the chosen offer declares via offer_required_step ([UC-ONB-02], Core entities: “Offer … declares required Steps”). The onboarding crate reads and snapshots that set at create; it never re-derives requirements from a hardcoded product taxonomy. Adding an offer or changing a flow is a catalogue/config change in offers, not new code here.

Design rule — the snapshot is immutable for the session’s life. Because the required Step set is snapshotted at create, an in-flight onboarding is unaffected by later catalogue edits. If the offer is retired or its Step set changes while an onboarding is in flight, the session is archived and the customer must start a fresh one ([UC-ONB-23]); the resolver never silently mutates a live requirement set ([UC-ONB-03]).

3. Boundaries — what onboarding owns vs references

Invariant — onboarding orchestrates, it does not own the catalogue or the banking objects. The catalogue crates (offers, subscriptions, customer_billing, accounting) are segment-neutral; onboarding is the cross-domain seam that depends on the domains it provisions into (Appendix A).

OwnsReferences (owned elsewhere)
The onboarding session + its status lifecycleThe offer and its offer_required_step set (offers)
The DataBag (transient working state, [UC-ONB-26])Identity / KYC verification (physical_person; PVID provider)
The Step dictionary, registry, and resolverAML / compliance decisions (compliance)
Validation routing + the validation freeze on the sessionThe banking account / IBAN / ledger (core_banking)
Provisioning saga orchestration ([UC-ONB-27])B2B organisation / KYB aggregates (organisation)
The onboarding adapters (INSEE, OTP, PVID)Subscriptions, billing, accounting (downstream commercial crates)

Design rule — DataBag is transient, not the source of truth. The DataBag is the working state of one onboarding: hydrated from the canonical customer/KYC master record at create (pre-fill, [UC-ONB-10]) and, on validation, frozen in place on the now-immutable session ([UC-ONB-05]/[UC-ONB-26]). A non-terminal session is archived/expired ([UC-ONB-15]); a validated one is retained as the provisioning record. The canonical record persists under the retention model ([UC-REG-08]).

Design rule — pre-fill excludes stale data. Data we already hold pre-fills the DataBag so the resolver skips Steps already satisfied; data that is stale (expired id, KYC refresh due) is excluded, so the resolver treats it as a gap and onboarding becomes the occasion to refresh it ([UC-ONB-10], Appendix A).

4. This round’s scope

This round generalizes the engine and delivers the sole-trader Step library end-to-end with onboarding-domain-real validation:

5. Related documents

10. Retail Mobile Integration (v1)

Retail Onboarding — Mobile Integration Guide (v1 POC)

Practical, implementation-focused instructions for the mobile app to drive the retail onboarding against retail_api. It documents the parts the generated OpenAPI SDK does not make obvious — the call sequence, the credential lifecycle, the two-phase steps, and error/resume handling. For exhaustive request/response schemas and exact HTTP paths, generate the typed client from the retail OpenAPI (/retail/openapi.json); this guide is the orchestration layer on top of it.

Scope: the POC, in two parts. The full POC (App-V2 board 10878-111709) runs: phone → email → name/DOB → address → nationality → US-person → device securing (mint) → offer choice (monthly/annual) → KYC questionnaire (professional situation, sector, source of funds, income) → PEP (self + entourage) → identity verification → account usage (utility, monthly volume, transfer zones) → submission → “account pending validation” home.

The two parts differ only in the credential model: Part 1 (§3) — everything up to the US-person gate — runs as an anonymous prospect (no minted session, no bearer token, no request-signing). Part 2 (§9) opens with secure_device, which is the mint and is itself still driven with the prospect credential — the bearer token arrives on its verify response; everything after it runs authenticated. The closed value lists of the questionnaire steps are in §9.3; §9.6 covers how each environment behaves.


1. The big picture

Full POC step order (retail)

get_phone → get_email → personal_identity → residence_address
→ nationality → us_citizen_declaration
── part 2 (see §9) ────────────────────────────────────────────
→ secure_device (THE MINT — still sent with the prospect token;
                 the bearer token arrives on its verify response)
→ confirm_offer → professional_situation   (authenticated from here)
→ activity_sector → source_of_funds → annual_income
→ pep_declaration → pep_entourage → id_doc_pvid
→ account_utility → monthly_volume → transfer_zones → submit_for_review

Part 1 ends when the response’s next.code is SecureDevice, or when status becomes Rejected (US person). See §6. Every part-2 step’s payload is specified in §9.


2. Credentials & headers

HeaderValueWhen
x-device-idthe server-issued device id from create_device (dvc_…)every call, from create_device onward
x-onboarding-prospectthe prospectToken returned by start_onboardingevery onboarding call after start

3. The call sequence (v1)

All calls are POST under the /retail prefix. Bodies below are the payloads; the SDK gives the exact operation paths.

3.1 Register the device — device/create_device (anonymous)

Do this once per install, before onboarding. Returns the deviceId you use as x-device-id.

// request
{
  "appVersion": "1.0.0",
  "platform": "ios",
  "osName": "iOS",
  "osVersion": "17.4",
  "deviceManufacturer": "Apple",
  "deviceModel": "iPhone15,3",
  "signingPublicKeySpkiB64": "<base64 SPKI of the device request-signing public key>"
}
// response → { "deviceId": "dvc_…", ... }   (see SDK for full shape)

The device must be a mobile device (it is, created here) to start a retail onboarding.

3.2 List offers — onboarding/list_offers (anonymous)

// request
{ "country": "FR" }
// response → [ { "id": "off_…", "code": "fr_retail_free", ... }, ... ]

The segment is fixed to retail by this API surface — you don’t send it. You don’t need this call to start (nor the country): start_onboarding defaults to the free offer and FR when the body is empty (§3.3), and the later offer screen uses sibling_offers (§9.2). Use list_offers only to show offers pre-start or deep-link into a specific one — and then resolve by code, never a hardcoded off_… id (ids are internal and can be renumbered; the code is the stable contract).

3.3 Start the onboarding — onboarding/start_onboarding (anonymous)

Send x-device-id. Returns the prospect token (store it) and the initial state whose currentStep is get_phone. Both body fields are optional on retail — the normal POC start sends the empty JSON object {} (with Content-Type: application/json; a truly bodyless request is rejected by the JSON parser):

list_offers (§3.2) applies the same FR default, so it too can be called with {} — but you don’t need it to start.

// request  (header: x-device-id: dvc_…, Content-Type: application/json)
{}
// response
{
  "prospectToken": "<opaque secret — store it, send as x-onboarding-prospect>",
  "onboarding": {
    "id": "onb_…",
    "status": "InProgress",
    "currentStep": { "code": "GetPhone", "title": "Phone", "prefill": null },
    "completedSteps": []
  }
}

3.4 Submit steps — onboarding/submit_step (owner-scoped)

Every submit sends x-device-id and x-onboarding-prospect, and this body:

{ "onboardingId": "onb_…", "step": { "stepCode": "<StepCode>", /* …fields */ } }

stepCode is the discriminator; its value + fields per v1 step:

StepstepCodePayload fields
PhoneGetPhone{ "phone": "+33…" } to issue, { "code": "123456" } to verify
EmailGetEmail{ "email": "…" } to issue, { "code": "123456" } to verify
IdentityPersonalIdentity{ "firstName", "lastName", "dateOfBirth": "YYYY-MM-DD" }
AddressResidenceAddress{ "addressLine1", "addressLine2"?, "postalCode", "city" } (country fixed FR)
NationalityNationality{ "nationalities": ["FR", "US", …] } (multi-select, ISO-3166-1 alpha-2)
US personUsCitizenDeclaration{ "isUsPerson": true | false }

The response is a SubmitStepResult — see §5.


4. Two-phase steps: phone & email OTP

get_phone and get_email are two calls each against the same stepCode:

  1. Issue — submit { "stepCode": "GetPhone", "phone": "+33…" }. The response is ok: true with next.code still GetPhone and status: "InProgress" — that’s your signal to show the code-entry screen.
  2. Verify — submit { "stepCode": "GetPhone", "code": "123456" }. On success the response advances (next.code = GetEmail).

Notes:


5. Response model

submit_step returns one of two shapes, discriminated by ok:

// success — step recorded, resolver advanced
{
  "ok": true,
  "next": { "code": "GetEmail", "title": "Email", "prefill": null } | null,
  "status": "InProgress",
  "token": ""   // OMITTED in v1 (only present at the phase-2 mint)
}

// validation failure — step NOT advanced, fix and resubmit
{
  "ok": false,
  "fieldErrors": { "phone": "is not a valid phone number" }
}

get_onboarding and start_onboarding.onboarding return an OnboardingState: { id, status, currentStep: Step | null, completedSteps: [StepCode] }.


6. The part-1 boundary

After us_citizen_declaration:

Drive both off the response: check status == "Rejected" first, then next?.code for what to render.


7. Resume (app relaunch / lost response)

On relaunch, call onboarding/get_onboarding with x-device-id + x-onboarding-prospect:

Because every step is idempotent to re-fetch and the backend is the source of truth for currentStep, the safe pattern after any dropped response is: call get_onboarding and render whatever it says.


8. Open decision that affects mobile — request signing

Today the app signs requests only from the secure_device mint onward (matching the current agreement). v1 never reaches the mint, so v1 needs no signing.

There is a proposed hardening (backend finding “A”) to require RFC-9421 request signing during the prospect phase too — i.e. sign every onboarding request from create_device onward — so a stolen prospect token alone can’t advance the flow. The device already registers its signing key at create_device, so the key is available; the change is when signing is enforced. This is a mobile-facing decision — if adopted, the app signs from device creation. It is tracked separately; until decided, implement v1 unsigned as above.


9. Part 2 — the rest of the POC

The same rules apply throughout (drive off currentStep, SubmitStepResult shape) — plus, from the mint onward, the bearer token and (in production only) request signing.

9.1 secure_device — the mint

The native passcode/Face ID section (not on the Figma board, it sits here). Two-phase device-key registration (TOFU): submit { "stepCode": "SecureDevice" } (empty) to get a challenge nonce in currentStep.prefill.challenge, sign it with a fresh P-256 device-auth key, then submit { "stepCode": "SecureDevice", "publicKeySpki": "…", "signature": "…" } (both base64 DER). This mints the session: the response’s token field carries the bearer token, and the prospect token is retired — from here send Authorization: Bearer <token> (and sign requests in production; staging does not enforce signing).

9.2 Offer selection — confirm_offer

Call onboarding/sibling_offers to list the choices: Free / Essentiel / Premium in monthly and annual variants (the annual toggle = picking the annual sibling’s offerId; premium is 12,90€/mois ↔ 130,20€/an). Then submit { "stepCode": "ConfirmOffer", "offerId": "off_…" } — re-selecting the current offer is a valid no-op.

9.3 The single-select steps

Seven screens share one payload shape: { "stepCode": "<Code>", "value": "<Value>" }. The values are closed sets carried in the OpenAPI schema as enums — the generated SDK types value as a union of the values below, so you get autocompletion and compile-time checking; an unknown value is a fieldErrors.value rejection. Values are PascalCase, like every enum on this contract (stepCode, status).

stepCodeAllowed values
ProfessionalSituationEmployed, SelfEmployed, Student, Unemployed, PublicServant, CompanyOwner, Retired
ActivitySectorRealEstate, PublicAdministration, Agriculture, ArtsEntertainment, OtherServices, Construction, SalesTrading, EnergyWater, EducationScience, FinanceInsurance, AccommodationCatering, Industry, InformationCommunication, Technology, HealthSocialServices, Transportation
SourceOfFundsSalary, Savings, InheritanceOrFamilySupport, Pension, Annuity, Other
AnnualIncomeUnder25k, From25kTo50k, From50kTo75k, From75kTo100k, From100kTo150k, Over150k
AccountUtilityMainAccount, SecondaryAccount, Savings
MonthlyVolumeUnder500, From500To1000, From1000To5000, Over5000
TransferZonesSepaOnly, OutsideEu

9.4 PEP (two screens)

Neither answer rejects the onboarding — a positive declaration is recorded for the compliance review.

9.5 Identity, submission, end state

9.6 Environments

While the product is not live to the public, the retail offers are active and the flow completes end-to-end on every stack:

10. Reference

2. Engine — Resolver, Registry, DataBag

Engine — Resolver, Registry, DataBag

This document defines the data-driven engine: the Step trait, the static StepRegistry, the DataBag/DataPoint model, and the resolver that computes the next missing Step. The engine decides what to ask next inside the InProgress/ChangesRequested lifecycle states; the status machine itself is in 4. Lifecycle.

1. Terminology

2. The Step trait

pub trait Step: Send + Sync + 'static {
    fn code(&self) -> StepCode;                   // stable string code, e.g. "siret_lookup"
    fn name(&self) -> &'static str;               // human label (catalogue, back office)
    fn kind(&self) -> StepKind;                   // DataCollection | Verification | Review | Submission
    fn provides(&self) -> &'static [DataPoint];
    fn requires(&self) -> &'static [DataPoint];
    fn applies_to(&self, offer: &OfferRef, bag: &DataBag) -> bool;
    fn on_complete<'a>(
        &'a self,
        bag: &'a DataBag,
        submitted: serde_json::Value,
        ctx: &'a StepContext,
    ) -> BoxFuture<'a, Result<DataBag, StepError>>;
}
pub type StepCode = &'static str;

name() and kind() make the registry the single source of the step catalogue: the back office browses steps derived from STEP_REGISTRY (code/name/kind), and there is no step_dictionary table. StepKind is a Rust-only type in offers.

Design rule — every Step is defined once and reused across offers. The same Step (e.g. id_doc_pvid) is the same registry entry wherever it appears; offers select from the dictionary, they never invent inline steps ([UC-ONB-02]). B2B and retail Steps share one dictionary, each gated by applies_to ([UC-ONB-13]).

Design rule — applies_to gates segment/conditional Steps. A Step in the required set is still skipped by the resolver when applies_to(offer, bag) is false. This is how one shared registry serves all segments: a B2B-only Step returns false for a retail offer, a conditional Step returns false until its trigger DataPoint is present ([UC-ONB-13]).

Design rule — on_complete is the only writer and must be idempotent. A Step validates the submission and returns the updated bag; resubmitting the same values must converge to the same bag and must not double-fire side effects ([UC-ONB-02] per-step submit protocol). On invalid input it returns a field-level StepError::ValidationFailed; the Step is not advanced and the FE shows the error ([UC-ONB-02]).

StepContext injects only the dependencies a Step needs (db pool, the adapter trait objects for INSEE/OTP/PVID — see 6. Integrations).

3. The Step registry

The registry is a process-static LazyLock<Vec<Box<dyn Step>>> populated from steps/mod.rs:

pub static STEP_REGISTRY: LazyLock<Vec<Box<dyn Step>>> = LazyLock::new(|| vec![
    Box::new(GetEmailStep),
    Box::new(GetPhoneStep),
    Box::new(PersonalInfoStep),
    Box::new(SiretLookupStep),
    Box::new(IdDocPvidStep),
    Box::new(RecapStep),
    Box::new(SubmitForReviewStep),
]);

Invariant — startup coverage assertion. At service registration the crate asserts that every Step referenced by every live offer’s offer_required_step set is present in STEP_REGISTRY; a missing Step is a boot panic, not a runtime dead-end:

fn assert_every_step_is_registered() {
    for offer in offers::all_offers() {
        for step_id in offer.required_step_ids() {
            assert!(STEP_REGISTRY.iter().any(|s| s.id() == step_id),
                "offer {} requires unregistered Step {step_id}", offer.id);
        }
    }
}

Design rule — deprecation ordering. A Step may leave the registry only after no live offer lists it and no in-flight session carries it in required_steps. Because each session snapshots its required set, removing a Step still present in a snapshot would make the resolver unable to advance that session. Ship the offer/Step-set change first; remove the Step after live sessions have drained or been archived ([UC-ONB-23]).

4. DataBag & DataPoint

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataBag(HashMap<DataPoint, BagValue>);

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BagValue { Text(String), Integer(i64), Bool(bool), Json(serde_json::Value) }

Design rule — JSONB on the session, typed at the Rust level. The bag is one jsonb column on onboarding_session (one row per session, atomic updates inside a transaction). The schema is enforced by the DataPoint enum in Rust, not by DB columns; keys serialise to the variant name (use #[serde(rename = "Old")] to survive a rename without a migration). All bag reads/writes go through the store.

Design rule — the bag stores references and values, never duplicates canonical truth. Ids and collected values live in the bag for the life of the onboarding; the canonical record is the source of truth, pre-filled in and flushed out at validation ([UC-ONB-26]). The sole-trader DataPoint set is enumerated in 3. Steps §3.

Design rule — pre-fill only for a known identity; an anonymous bag starts empty. Pre-fill hydrates the bag from canonical KYC only when the owner is a known identity — an authenticated user, or an anonymous session later claimed by one ([UC-ONB-28]/[UC-ONB-30]). A purely anonymous onboarding has no canonical record to read, so its DataBag begins empty and every required Step is a genuine gap ([UC-ONB-10]/[UC-ONB-26]).

5. The resolver

The resolver is a pure function of the snapshot, the bag, and the officer-injected unmet_steps:

flowchart TD
    A[get_current_step] --> B{status InProgress\nor ChangesRequested?}
    B -- no --> Z[return per status:\nUnderReview / Validated / …]
    B -- yes --> C[required = session.required_steps snapshot]
    C --> D[satisfied = steps whose `provides`\nare all present+valid in bag]
    D --> E["gap = required − satisfied\n∪ unmet_steps (officer remediation)"]
    E --> F[drop Steps where\napplies_to offer,bag is false]
    F --> G{gap empty?}
    G -- yes --> H[return None ⇒ ready to submit]
    G -- no --> I[topological sort gap by `requires`]
    I --> J[return first Step\nwhose `requires` are all satisfied]
pub fn resolve(
    offer: &OfferRef,
    required_steps: &[StepId],   // session snapshot ([UC-ONB-02])
    unmet_steps: &[StepId],      // officer remediation, always re-surfaced ([UC-ONB-06])
    bag: &DataBag,
) -> Option<StepId> {
    let satisfied = bag.satisfied_steps();
    let gap: Vec<StepId> = required_steps.iter().copied()
        .filter(|s| unmet_steps.contains(s) || !satisfied.contains(s))
        .filter(|s| step(s).applies_to(offer, bag))
        .collect();
    topo_sort_by_requires(&gap, bag).into_iter().next()
}

Design rule — the resolver is required − satisfied, topo by requires. It never special-cases the source of a DataPoint: a value the customer typed, a value pre-filled from canonical data, and a value injected by an officer remediation are indistinguishable to the resolver ([UC-ONB-03], [UC-ONB-06]).

Design rule — the resolver and registry are owner-agnostic. They reason only over the snapshot, the bag, and unmet_steps; whether the owner is an anonymous prospect or an authenticated user changes nothing here ([UC-ONB-28]). The only owner-dependent input is pre-fill (empty for anonymous, canonical for known identity, §4), which simply makes more Steps satisfied for a known identity.

Design rule — officer remediation is just an unmet Step. A partial rejection ([UC-ONB-06]) marks the affected Step(s) in unmet_steps; the resolver re-surfaces them as the next gap. There is one re-entry path — first visit, resume after dropping out, or return after partial rejection are the same flow ([UC-ONB-06]). Re-entry mechanics are in 4. Lifecycle.

Invariant — None means ready to submit, not “done”. When the gap is empty the resolver returns None; the session may then be submitted for review (4). Provisioning happens only at validation (5), never as a resolver side effect.

Design rule — resume is deep-linkable. get_current_step returns the next Step id plus its prefill; landing on /{onboardingId} redirects to /{onboardingId}?step=<next-missing>, recomputed after every submit ([UC-ONB-03]). State lives server-side, so cross-device resume works for free ([UC-ONB-17]).

6. Related documents

3. Step Dictionary & Sole-Trader Steps

Step Dictionary & Sole-Trader Steps

This document defines the shared Step dictionary ([UC-ONB-02]) and the concrete Steps that make up the sole-trader (auto-entrepreneur / micro-entreprise) onboarding delivered this round, with each Step’s provides, requires, applies_to, and adapter.

The retail flow reuses this same dictionary and adds its own Steps (us_citizen_declaration, secure_device, pep_entourage, and the single-select questionnaire/account-usage Steps in steps::select). The authoritative retail step order is the catalogue (offers::catalogue::RETAIL_REQUIRED_STEPS); the consumer-facing walkthrough is 10. Retail Mobile Integration.

1. The shared dictionary

Design rule — one dictionary, per-offer selection. There is a single registry of reusable Steps, each defined once and reused across offers. Every offer declares the subset of Steps it requires (offer_required_step, owned by offers); it never invents inline steps ([UC-ONB-02]). The dictionary spans segments — B2B and retail Steps coexist — each gated by applies_to(offer, bag) ([UC-ONB-13]). Adding a marketing capability ([UC-ONB-24]) is configuring an offer’s Step subset, not building a flow.

Design rule — the dictionary lives only in code. There is no step_dictionary database table: each Step’s code, name and kind (StepKind, a Rust-only type in offers) are typed methods on the Step trait, and the catalogue the back office browses is derived from the STEP_REGISTRY. offer_required_step references Steps by step_code (text), and the boot assertion assert_registry_covers_offers fails fast if an offer references a code the registry does not define.

The dictionary envisaged by [UC-ONB-02] includes (not all built this round): get_email (verify email, [UC-ONB-31]), get_phone (verify phone, [UC-ONB-31]) — the two contact steps run in the offer’s snapshot order (email-first B2B, phone-first retail) and the session is minted when the contact set completes — personal_info, id_doc/PVID, proof_of_address, funds_origin, topup, parental_consent, siret_lookup, kyb_docs, recap, submit_for_review. This round builds the sole-trader subset (§2); the rest are dictionary entries to be implemented as their offers are scoped.

2. The sole-trader Step set

The sole-trader offer’s offer_required_step snapshot, in resolver topological order (each later Step requires the earlier outputs). On the unauthenticated / anonymous path the order is get_email_password, get_phone, personal_info, siret_lookup, id_doc_pvid, pep_declaration, confirm_offer, recap, submit_for_review (the B2B flow opens with get_email_password; retail opens with the passwordless get_email); an authenticated onboarding skips get_email and get_phone (identity is already known and verified, [UC-ONB-28]):

Step (code)providesrequiresAdapter
get_email_passwordContactEmail, EmailVerified (+ stages the password credential for the contact-set mint)EmailSender (OTP, stubbed)
get_phonePhoneVerifiedchannel-selected: PhoneVerifier (web → Vonage, stubbed; mobile → Infobip silent-auth, deferred)
personal_infoPersonalFirstName, PersonalLastName, PersonalDateOfBirth, PersonalNationalitynone
siret_lookupSiret, LegalName, LegalForm, CompanyAddress, ApeCodePersonalLastNameSiretLookupProvider (real INSEE)
id_doc_pvidIdentityVerificationStatusPersonalFirstName, PersonalDateOfBirthIdentityVerificationProvider
pep_declarationIsPep (+ PepCategory, PepFamilyMember, PepCloseAssociate)PersonalLastNamenone
confirm_offerOfferConfirmed(all of the above)none — reads the offers catalogue for siblings
recapRecapConfirmed(all of the above)none
submit_for_review(transition Step)RecapConfirmednone

Design rule — applies_to for the sole-trader set. Every Step above returns true only for sole-trader (and compatible B2B-pro) offers; on a retail offer the B2B Steps (siret_lookup) return false and are skipped, even if some shared catalogue placed them in scope ([UC-ONB-13]). personal_info, id_doc_pvid, and recap are shared with retail flows. get_email and get_phone are gated to the unauthenticated path — they open the flow in the offer’s snapshot order when the onboarding starts logged-out and are skipped when the onboarding is authenticated, since the owner’s email/phone are already known and verified ([UC-ONB-28]).

2.1 get_email (verify email — unauthenticated path)

Design rule — the contact steps verify email + phone, and the verification that completes the set mints the authenticated session. For an onboarding started from the logged-out surface ([UC-ONB-01]), the offer’s snapshot orders the two contact steps. get_email is a two-phase Step:

  1. Issue. The prospect submits an email; the backend runs the duplicate-identity check on the email ([UC-ONB-32]). The on-screen response is the same either way“we’ve sent a code to your email” — and the Step stays unmet; the divergence is delivered only in the inbox: a fresh identity receives a one-time code (an EmailSender adapter — stubbed for now; an OTP, not a magic link), its hash + expiry stored in the bag; an existing identity instead receives a “you already have an account — log in” email and no onboarding code, so the Step can’t be completed and the real owner is routed to login. This keeps the flow enumeration-safe ([UC-ONB-32]).
  2. Verify. The prospect submits the code. On a match within the window the backend sets EmailVerified in the bag. When that verification completes the contact set (both EmailVerified and PhoneVerified) on an anonymous session, the backend (a) creates a real minimal user + verified email AND phone login-identifiers, (b) mints an authenticated session via the authentication crate — exactly as a successful login would — passwordless: the session records a CredentialKind::EmailOtp (no password, no credential-envelope row), delivered as the standard httpOnly session cookie/token, (c) promotes the onboarding owner from the prospect token to the new user_id ([UC-ONB-28]/[UC-ONB-29]).

Once the contact set completes the caller is authenticated, not anonymous. Because no account exists yet the session still grants no access to any real data ([UC-ONB-31]).

Design rule — an authenticated onboarding skips get_email. When the onboarding starts inside the app the owner is a known user_id ([UC-ONB-28]) whose email is already held and verified, so applies_to returns false and the Step is skipped (the value comes from canonical pre-fill, [UC-ONB-10]).

2.1b get_email_password (verify email and set a password — B2B variant)

Design rule — the B2B (sole-trader web) flow opens with get_email_password, a drop-in variant of get_email that also sets a password. It provides the same points (ContactEmail, EmailVerified) so it slots into the resolver and the authenticated-path skip identically; the sole-trader offer requires it in place of get_email, while retail keeps the passwordless get_email. This exists because the B2B web app lets customers log in with email + password — a deliberately lower-security method we expect to drop eventually. Two phases like get_email:

  1. Issue. { email, password, confirmPassword }. Validates the password (min length; confirmPassword must match), runs the same enumeration-safe duplicate-identity check ([UC-ONB-32]), and argon2-hashes the password — always, on both the fresh and existing-account branches, so hashing cost keeps timing flat. The hash is held transiently in the bag as the secret EmailPasswordCredentialHash (never logged, [UC-ONB-35], and stripped from the back-office view); the code is issued exactly as get_email.
  2. Verify. { code }. On a match, EmailVerified is set; the mint at contact-set completion additionally creates a durable Password credential (from the stored hash) alongside the verified login-identifiers, records the session as CredentialKind::Password, and removes the transient hash from the bag. The plaintext password never leaves the step.

On the authenticated path the bag is pre-seeded (ContactEmail + EmailVerified), so — like get_email — the step is skipped and a returning customer is not asked to set a password again. The password credential is a normal credential, so archival’s purge_user_identity hard-deletes it with the rest of the transient identity bootstrap when an onboarding is abandoned ([UC-ONB-15]).

2.2 personal_info (no adapter)

Collects the person’s legal name, date of birth, and nationality. Pure validation; writes PersonalFirstName/PersonalLastName/PersonalDateOfBirth/ PersonalNationality to the bag. No external call. Pre-fillable from canonical data for an existing customer ([UC-ONB-10]).

2.3 siret_lookup (real INSEE)

Design rule — SIRET is resolved against the real INSEE/Sirene API. The Step calls SiretLookupProvider, which reuses clients::insee::InseeApi::get_company_data(siret) (6. Integrations §2); credentials (PARAMETERS.insee.api_key) already exist. On success it autofills LegalName, LegalForm, CompanyAddress, and ApeCode from the registry response; on a not-found / inactive SIRET it returns StepError::ValidationFailed and the Step is not advanced ([UC-ONB-02]). on_complete is idempotent: re-submitting the same SIRET re-fetches and converges to the same bag values.

2.4 id_doc_pvid (identity verification)

Design rule — identity verification goes through the IdentityVerificationProvider trait. The Step starts a PVID check via the provider and records IdentityVerificationStatus in the bag. The real provider is Ubble (acquired by Checkout.com, now “Checkout.com – Identity Verification”), using Ubble’s Certified / PVID configuration; alongside it is a config-gated sandbox implementation for dev/test (6. Integrations §4). The vendor contract is documented in the Checkout (Ubble) PVID integration doc.

⚠️ Launch-blocking until the final wire lands. The Ubble integration is mostly built (checkout_pvid crate, clients::checkout transport, webhook service, back-office routes, migrations; mTLS + signature keys in parameters/checkout.rs). What remains: the live Ubble Basic credentials + user_journey_id, and reshaping the onboarding seam from the synchronous verify() to start + webhook-driven completion so checkout_pvid plugs in. Until then only the sandbox path completes the Step. Remaining work is tracked in the Checkout (Ubble) PVID integration §12–§13.

Design rule — a successful PVID is non-editable at recap. Once IdentityVerificationStatus is a successful verification it is shown at recap but cannot be edited there ([UC-ONB-04]); correcting regulated identity data requires re-verification through the compliance gate ([UC-ONB-04]/[UC-REG-02]), not a free edit. A failed check ([UC-ONB-14]) leaves the Step unmet; retry per policy.

2.5 get_phone (verify phone — channel-selected provider)

Design rule — phone collection and verification are one Step, behind a channel-selected provider. get_phone is the second Step on the logged-out path (requires EmailVerified, so the caller is already authenticated). It is a two-phase Step:

  1. Issue. The prospect submits a phone; the backend runs the duplicate-identity check on the phone ([UC-ONB-32]). As at get_email, the on-screen response is invariant (“we’ve sent a code to your phone”) and the Step stays unmet; the divergence is delivered only over SMS — a fresh number gets a one-time code via the PhoneVerifier selected by the request channel ([UC-ONB-33]) (web → Vonage OTP, self-issued hash+expiry in the bag, SMS via messaging/vonage, 6. Integrations §3stubbed; mobile → Infobip silent network auth — deferred), while a number already in use gets a “this number is already linked to an account” SMS and no code, keeping the flow enumeration-safe.
  2. Verify. On a correct code the backend adds a verified phone login-identifier to the now-authenticated user and sets PhoneVerified (and records the VerificationChannel). No new session is minted — get_email already established it.

This Step replaces the former separate mobile_otp Step: phone collection (formerly in contact_details) and verification are folded together here. The OTP model is self-issued (decided): Green-Got generates and verifies the code; the provider only delivers it (§2.1 and 6. Integrations §3).

2.6 pep_declaration (PEP self-declaration — no adapter)

Design rule — PEP status is captured as a regulated self-declaration Step. pep_declaration collects the customer’s politically-exposed-person status ([UC-ONB-36]) feeding the ongoing PEP/adverse-media compliance gate ([UC-REG-02]). Pure validation, no external call. It writes IsPep (bool) always; when IsPep is true it additionally requires and writes PepCategory (the public function / category held) and captures the PepFamilyMember and PepCloseAssociate flags. It requires PersonalLastName (so the declared person is identified), placed immediately after identity verification (id_doc_pvid). It is required by the sole-trader offers today; on retail it is temporarily deferred to a post-offer KYC questionnaire (not yet built) and MUST be back in RETAIL_REQUIRED_STEPS before retail validation goes live — enforced by the fail-closed officer_validate (no retail provisioning path yet) and the separate retail_lifecycle gate. A positive declaration does not block the Step — it is surfaced to the compliance officer at review ([UC-BO-12]) for enhanced due diligence, not auto-rejected. Screening the declared identity against sanctions/PEP watchlists is a separate regime ([UC-REG-02]), out of scope here.

2.7 confirm_offer (confirm / switch offer)

Design rule — confirm the offer, optionally switch to a sibling, right before recap. The Step lists the onboarding’s current offer plus its siblings (same group_code, [UC-OFF-15]) with their prices, and lets the customer switch before review. The submission carries { offerId }: the current offer (a no-op confirmation) or a sibling. The engine validates offerId is the current offer or one of its siblings (else a field error on offerId), and when it differs updates onboarding_session.offer_id atomically with the Step. Because siblings share an identical Step set, the snapshotted required_steps and every captured DataPoint stay valid — nothing is re-asked. Confirming sets OfferConfirmed. [UC-ONB-34]

2.8 recap (confirmation)

Design rule — onboarding always ends with a recap. The recap summarises the captured data for the customer to confirm — including pre-filled data shown here even though it was not asked as a Step ([UC-ONB-04]). A successful PVID is non-editable (§2.4). Editing a field re-opens any dependent Step (the correction clears the relevant DataPoint, so the resolver re-surfaces that Step). Correcting pre-filled data emits an update event that refreshes the canonical record — the recap doubles as a maintenance point ([UC-ONB-04]). Confirming sets RecapConfirmed.

2.9 submit_for_review (transition Step)

A transition Step with no DataPoints to provide: it requires RecapConfirmed and, when reached with an otherwise-empty gap, flips the session InProgress → Submitted (4. Lifecycle). It is the resolver’s terminal Step for the flow.

3. Sole-trader DataPoint set

pub enum DataPoint {
    // get_email / get_email_password (unauthenticated path; verify email → mint
    // user/session [UC-ONB-31]). EmailPasswordCredentialHash is the B2B-only secret
    // transient argon2 hash, consumed at the mint.
    EmailVerified, EmailOtpChallengeHash, EmailOtpChallengeExpiresAt, EmailPasswordCredentialHash,
    // get_phone (channel-selected verification [UC-ONB-33])
    PhoneVerified, VerificationChannel,
    // personal_info
    PersonalFirstName, PersonalLastName, PersonalDateOfBirth, PersonalNationality,
    // siret_lookup
    Siret, LegalName, LegalForm, CompanyAddress, ApeCode,
    // id_doc_pvid
    IdentityVerificationStatus,
    // pep_declaration (regulated self-declaration [UC-ONB-36])
    IsPep, PepCategory, PepFamilyMember, PepCloseAssociate,
    // recap
    RecapConfirmed,
}

The verified email/phone values are the durable login-identifiers on the authentication side; the bag carries the verification facts (EmailVerified, PhoneVerified), the transient email-OTP challenge (EmailOtpChallengeHash + EmailOtpChallengeExpiresAt, cleared on verify), and the VerificationChannel. All ride in the JSONB data_bag — no column migration.

Invariant — a Step is satisfied iff all its provides DataPoints are present and valid. Stale or invalidated values (an expired pre-fill, an officer-marked unmet Step) are treated as absent, so the resolver re-surfaces the Step ([UC-ONB-06]/[UC-ONB-10]). See 2. Engine §5.

4. Related documents

4. Onboarding Lifecycle & Status

Onboarding Lifecycle & Status

This document defines the session status state machine and the transitions that drive it: create, submit, validate, request-changes, resume, abandon, and expire. The status axis is orthogonal to the resolver, which decides which Step inside InProgress/ChangesRequested (2. Engine).

1. The status state machine

stateDiagram-v2
    [*] --> Draft: create_onboarding\n(snapshot offer_required_step, pre-fill bag) [UC-ONB-02]
    Draft --> InProgress: first get_current_step / first submit
    InProgress --> InProgress: submit_step (resolver advances) [UC-ONB-03]
    InProgress --> Submitted: submit_for_review (resolver gap empty) [UC-ONB-02]

    Submitted --> UnderReview: officer picks up / auto-validation routing [UC-ONB-25]

    UnderReview --> Validated: validate → freeze session (stamp officer + time) [UC-ONB-05]
    note right of Validated
        Anonymous owner is promoted here:
        validate creates the user, owner
        prospect_token → user_id [UC-ONB-29]
    end note
    UnderReview --> ChangesRequested: request_changes\n(mark Step(s) unmet + OfficerComment) [UC-ONB-06]

    ChangesRequested --> InProgress: resume (one universal re-entry) [UC-ONB-06]
    InProgress --> Submitted: re-submit after remediation

    UnderReview --> Rejected: officer full reject (AML/fraud) [UC-ONB-07]
    Submitted --> Rejected: officer full reject (AML/fraud) [UC-ONB-07]
    InProgress --> Rejected: system eligibility hard-stop (US person) [UC-ONB-36]
    ChangesRequested --> Rejected: system eligibility hard-stop (US person) [UC-ONB-36]

    Draft --> Abandoned: customer abandon
    InProgress --> Abandoned: customer abandon
    ChangesRequested --> Abandoned: customer abandon

    Draft --> Expired: TTL [UC-ONB-15]
    InProgress --> Expired: TTL [UC-ONB-15]
    ChangesRequested --> Expired: TTL [UC-ONB-15]

    Validated --> [*]: terminal (provisioned)
    Rejected --> [*]: terminal (no provisioning)
    Abandoned --> [*]: terminal (archived)
    Expired --> [*]: terminal (archived)

States: Draft, InProgress, Submitted, UnderReview, Validated, ChangesRequested, plus the terminals Rejected, Abandoned, Expired.

Design rule — the happy path is Draft/InProgress → Submitted → UnderReview → Validated. Content is gathered in InProgress (resolver-driven); submit_for_review moves to Submitted; review routing moves to UnderReview; validation moves to Validated, freezing the session as the immutable provisioning record ([UC-ONB-05], 5. Validation).

Design rule — ChangesRequested loops back through InProgress. A partial rejection does not create a special state of its own beyond flagging: it marks the affected Step(s) unmet, attaches an OfficerComment, and the session re-enters InProgress on resume so the resolver surfaces exactly those Steps ([UC-ONB-06]). Re-submitting returns to Submitted normally.

2. Transitions

2.1 create_onboarding (→ Draft)

The FE requests creation for a chosen offer; only the backend writes ([UC-ONB-02]). The use case binds the owner ([UC-ONB-28]): an in-app authenticated caller becomes the user_id owner with the DataBag pre-filled from non-stale canonical data ([UC-ONB-10]); a logged-out caller becomes an anonymous owner whose prospect session is minted up front and carried as an httpOnly cookie ([UC-ONB-28]/[UC-ONB-31]), its DataBag starting empty — the get_email Step (§2.8) then verifies the email and promotes the owner to a real user_id. It then enforces one in-flight onboarding per owner ([UC-ONB-03]/[UC-ONB-28], see §3), snapshots the offer’s offer_required_step set into required_steps, sets expires_at from the offer’s TTL ([UC-ONB-15]), and emits OnboardingCreated. Onboarding is always tied to one offer (a bundle is a single offer ⇒ one onboarding) ([UC-ONB-02]).

2.2 submit_step (InProgress → InProgress)

Each completed Step is submitted to the backend, which validates it and responds invalid (field-level error, Step not advanced) or valid (recorded in the bag, next Step returned by the resolver) ([UC-ONB-02]). The resolver re-runs after every submit ([UC-ONB-03]); OnboardingStepCompleted is emitted per Step.

2.3 submit_for_review (InProgress → Submitted)

Allowed only when the resolver returns None (no gap). Sets submitted_at, emits OnboardingSubmitted.

2.4 validate / request_changes / reject (from UnderReview)

Validation routing (officer always; migration excepted) is in 5. Validation §1 ([UC-ONB-25]). validateValidated (freezes the session, stamping validated_at + validated_by, [UC-ONB-05]); request_changesChangesRequested ([UC-ONB-06]); rejectRejected, terminal, no provisioning ([UC-ONB-07]). All BO actions are audited with a reason code ([UC-BO-10]). Four-eyes (manager) approval is scoped to manual data modifications/overrides ([UC-BO-09], not implemented in this slice); a plain validation of a complete submission does not require co-approval.

2.5 resume (one universal re-entry)

Design rule — there is one re-entry mechanism, not a special rejection path. You open the app, authenticate, the FE asks the backend, the backend says “you have an ongoing onboarding at Step X”, and you are routed to it. The cause is irrelevant — first connection, resuming after dropping out, or returning after a partial rejection are the same flow ([UC-ONB-06]). The FE chooses presentation: no current subscription ⇒ show the onboarding directly; existing products ⇒ show the homepage plus a continue link ([UC-ONB-03]).

Design rule — resume differs by owner kind. An authenticated session is keyed to the user_id and resumes across devices for free, since state lives server-side ([UC-ONB-17]). An anonymous session is reachable only while its prospect-token cookie is held; losing the cookie abandons the session ([UC-ONB-15]/[UC-ONB-28]). Cross-device anonymous resume requires the prospect to authenticate and claim the session (§2.7) ([UC-ONB-30]).

2.6 abandon / expire

abandon is a customer action from any non-terminal state ⇒ Abandoned.

Design rule — expiry archives, it does not delete. An onboarding left incomplete past its TTL (defined on the offer) is marked Expired and archived, not deleted ([UC-ONB-15]). Re-entry starts a fresh session, which may re-pre-fill from any persisted, non-anonymised person ([UC-ONB-15]/[UC-ONB-16]). Retention/anonymisation then follow the purpose-based model ([UC-REG-08]). A daily scheduler job performs the archival (8. Architecture).

Archival cleanup — hard-delete the transient identity, keep the fraud signals. Because an archived onboarding never provisioned an account, archival (officer Abandoned today; the daily expiry sweep / customer-abandon when built) hard-deletes the transient identity bootstrap the onboarding created, atomically with the status flip: the throwaway app_user, its sessions, OTP attempts, any credential, and the email/phone login identifiers (freeing the unique email/phone for re-onboarding). It keeps the fraud signals — KYC identity verifications, any physical person, and the session row + DataBag. Every archival path MUST route through use_cases::archive::cleanup_bootstrapped_identity so this stays uniform.

Two guards make it safe: (1) it only touches a user the onboarding itself minted (onboarding_session.bootstrapped_user_id, set at [UC-ONB-31] promotion; NULL on the authenticated path ⇒ a pre-existing user is never deleted); (2) it keeps any user who became a real customer (validated some onboarding). Cleanup is an explicit allowlist (unclassified data is never auto-deleted) and the app_user FKs are the loud-fail backstop. Adding a step that persists outside the DataBag? Classify each artifact TRANSIENT or FRAUD SIGNAL per [UC-ONB-15].

2.7 ownership transitions (promotion / claim)

The owner is orthogonal to status: it is set at create and, for an anonymous session only, switches from prospect_token to a user_id at one of two moments.

Design rule — promotion at validation. An anonymous onboarding has no user until validation ([UC-ONB-28]). At validation the Provisioner creates the new user/login from the validated DataBag and the owner is promoted from the prospect token to that new user_id; the prospect token is then retired and the customer continues authenticated ([UC-ONB-29], see 5. Validation §2). An authenticated onboarding reuses its existing user ([UC-ONB-05]).

Design rule — claim on sign-in. A prospect who authenticates mid-flow claims the session — owner switches to the user_id and the DataBag is enriched with canonical pre-fill for still-missing, non-stale points — only if the user has no other in-flight onboarding. If they already have one in flight, the two are not silently merged → route to customer support; this preserves the one-in-flight-per-owner invariant across the identity transition ([UC-ONB-30], [UC-BO-08]).

Design rule — claim requires an exact identity match (anti-abuse). A claim is permitted only if the identity already captured in the anonymous DataBag matches the authenticating user’s canonical identity — the collected name and date of birth must equal the signing-in user’s. On a mismatch the claim is refused: the session is not attached, the customer is routed to support ([UC-BO-08]), and the anonymous DataBag is discarded (not grafted onto the account). This blocks collecting (or PVID-verifying, [UC-ONB-04]) one person’s identity anonymously and attaching it to a different signed-in account ([UC-ONB-30]/[UC-REG-02]). The match is exact by default; loosening it (fuzzy match + re-verification through the compliance gate) is a later compliance decision, not a silent relaxation.

2.8 anonymous entry: prospect-session minting, email verification & promotion

Design rule — create_onboarding mints the prospect session; completing the contact set upgrades it to a real authenticated session. On the logged-out path picking an offer mints the prospect session (httpOnly prospect-token cookie, empty DataBag). The two contact steps run in the offer’s snapshot order (email-first B2B, phone-first retail, 3. Step Dictionary §2.1); the OTP verification that completes the set creates the user + both verified identifiers, mints an authenticated session and promotes the owner prospect→user_id ([UC-ONB-28]/[UC-ONB-31]). Until then no account exists, so the prospect session grants no access to any real data and cannot be a “stolen session”. An authenticated onboarding skips both contact steps — identity is already known and verified ([UC-ONB-28]).

Design rule — a duplicate identity is enumeration-safe: invariant on screen, signal in the channel. If the submitted email (at get_email) or phone (at get_phone) already belongs to an existing login-identifier, the backend creates no user / identifier / session and does not advance the Step — but the on-screen response is identical to the fresh-identity case (“we’ve sent a code to your email/phone, enter it”; same wording, same next screen, comparable timing). The “you already have an account” signal is delivered only through the contact channel the real owner controls: a fresh identity receives the onboarding OTP; an existing identity instead receives a “log in / recover access” message (a login link) and no onboarding code, so the Step can’t be completed and the genuine owner is routed to login. A stranger probing addresses they don’t control sees the same screen either way, so existence is not leaked ([UC-ONB-32]). This is the not-yet-authenticated counterpart of the mid-flow sign-in claim (§2.7, [UC-ONB-30]). Holding the property requires timing parity, rate-limit + CAPTCHA on the contact Step, and the same no-leak posture on login / credential-recovery ([UC-REG-02]).

Implemented this way: the contact Step (get_email/get_phone) returns the same “code sent” outcome for an existing identity as for a fresh one, storing a decoy challenge — a hash no submitted code can satisfy — so the bag shape and the verify failure are byte-indistinguishable from a fresh issue; the existing-account message is sent through the EmailSender/OtpSender adapter’s send_existing_account_notice. The old on-screen AlreadyHasAccount outcome is removed. Only the message content awaits the real email/SMS transport (today the sender is a stub that logs).

3. Invariants

Invariant — at most one in-flight onboarding per owner. Enforced by a partial unique index on user_id and a parallel one on prospect_token, both over the non-terminal statuses (Draft, InProgress, Submitted, UnderReview, ChangesRequested) — see 7. Data Model. The rule holds per owner whether anonymous or authenticated, and is preserved across a claim ([UC-ONB-30]). Starting a second offer is sequential ([UC-ONB-03]/[UC-ONB-28]/[UC-J-03]).

Invariant — authorization matches the caller to the owner. Every read/resume/submit/edit/submit-for-review matches the caller’s identity (prospect token or user_id) to the session owner; a mismatch is not found ([UC-ONB-28]).

Invariant — the requirement set is fixed for the session’s life. The required_steps snapshot taken at create is never mutated by the resolver. If the offer is retired or its Step set changes mid-flight, the session is archived and the customer starts fresh ([UC-ONB-23]); if the last customer-provided info is < 7 days old, notify, else archive silently ([UC-ONB-23]).

Invariant — provisioning happens only at Validated. No account or customer record exists before validation, and the session is the provisioning record only once frozen as Validated ([UC-ONB-05]). Rejected and Expired provision nothing.

Invariant — terminal states are terminal. Validated, Rejected, Abandoned, and Expired have no outgoing transitions; re-engagement is always a new session ([UC-ONB-15]/[UC-ONB-16]).

4. Related documents

5. Validation & Provisioning

Validation & Provisioning

This document defines what happens when a submitted onboarding is validated: the officer-always routing ([UC-ONB-25]), the validation freeze that makes the session itself the durable provisioning record ([UC-ONB-05]), and the idempotent, resumable provisioning saga ([UC-ONB-27]).

1. Validation routing — compliance officer always (migration excepted)

Design rule — every onboarding is finalized by a compliance officer ([UC-ONB-25]). There is no automatic validation of a real onboarding: a submitted onboarding always waits for a compliance officer to validate it before provisioning. This holds across segments and offers — free or paid, retail or B2B, first-time or returning. Automatic validation still runs the compliance gates ([UC-REG-02]) as a pre-check, but a human officer owns the final decision.

The sole automated exception — migration. The agent-PI → independent-PI customer-base migration ([UC-MIG-03]) is mechanically an automated onboarding: its DataBag is built from the customer’s existing, already-KYC’d legacy data, so it is validated automatically without an officer. No other path is auto-validated.

Decided. There are no auto-vs-manual risk thresholds to tune — the routing is “officer always, migration ([UC-MIG-03]) excepted”.

Invariant — validation only acts on a complete submission. validate is allowed only from {Submitted, UnderReview} and only when the resolver returns None (no gap). A gap re-routes to request_changes, not validation.

2. The validated session is the provisioning record

Design rule — validation freezes the session in place; there is no separate provisioned-customer entity. On validation the crate flips the session to Validated and stamps the validating officer and timestamp on the session row itself (validated_at / validated_by). The session becomes immutable: it is the durable, queryable record that validation happened — its frozen data_bag is the snapshot of exactly what was approved, and OnboardingValidated (carrying the onboarding_id) is the seam other commercial crates subscribe to. There is no ProvisionedCustomer row.

The validation metadata added to OnboardingSession:

pub status: OnboardingStatus,            // -> Validated (terminal, immutable)
pub validated_at: Option<DateTime<Utc>>, // Some iff Validated
pub validated_by: Option<String>,        // operator id of the validating officer

What validation provisions now. For a sole trader (EI) onboarding, the validate use-case provisions the real downstream aggregates on the same transaction as the validation freeze ([UC-ONB-05], see use_cases/provisioning.rs and use_cases/back_office.rs): it maps the frozen data bag onto an organisation (created Active, KYB Approved by the validating officer) and then a core_banking bank_account linked to it. Provisioning is the anti-corruption boundary that translates the bag into the organisation / core_banking provisioning commands; a missing required data point is an invariant violation (the resolver gates them before submission), not a user path. Non-sole-trader offers are not provisioned yet — those product families are wired in a later round.

There is still no separate ProvisionedCustomer record: provisioning writes directly into the referenced domains’ own aggregates and stays atomic with the freeze, so a dedicated onboarding-owned row would only duplicate them. A provisioning record earns its place later — when provisioning becomes a multi-step cross-domain saga that needs its own identity and progress separate from the validation decision; until then the OnboardingValidated event is the contract downstream domains react to, regardless of what backs it.

Design rule — the frozen session is the snapshot. The DataBag is transient ([UC-ONB-26]), but a Validated session is terminal and never edited, so its data_bag is permanently the approved snapshot. Archival/expiry apply only to non-terminal sessions ([UC-ONB-15]); a validated one is retained.

Design rule — the user is created early (at get_email); validation freezes the session ([UC-ONB-29]). Promotion happens in two moments. The user + verified email/phone login-identifiers + authenticated session are created at the get_email Step ([UC-ONB-31]), so by validation the owner is already a real user_id (the prospect token was retired then). Validation is the moment the prospect becomes a customer: the validate use-case reuses the existing user (and, when real provisioning lands, creates the person, account(s), card(s) and subscription from the validated DataBag). For an authenticated onboarding the user likewise already exists and is reused ([UC-ONB-05]). No customer-facing entity is created before validation — only the thin auth shell is created up front.

Resolved (was ONB-U6 credential bootstrapping). The brand-new prospect authenticates passwordlessly via the email/phone OTP during get_email/get_phone, which both creates the durable login-identifiers and mints the session ([UC-ONB-31]). A first-class password/passkey credential remains a later, optional enrolment owned with the authentication domain, but is no longer a blocker.

3. The provisioning saga ([UC-ONB-27])

Provisioning is a multi-step saga that can partially fail. This round’s “saga” is a single step — freeze the session as Validated (promoting an anonymous owner first) and emit the event — but it is described against the saga contract so later steps (real person/account/subscription/first invoice) slot in unchanged.

sequenceDiagram
    autonumber
    participant OFF as Officer (or migration auto-validator)
    participant UC as validate_onboarding
    participant SES as onboarding_session store
    participant BUS as EVENT_BUS

    OFF->>UC: validate(onboarding_id, officer_id)
    UC->>UC: guard: status ∈ {Submitted, UnderReview} && resolver == None
    Note over UC: already Validated ⇒ no-op, return frozen session (idempotent)
    alt anonymous owner (prospect_token set) [UC-ONB-29]
        UC->>UC: create new user/login from validated DataBag;\nowner prospect_token → user_id; retire token
    else authenticated owner (user_id set)
        UC->>UC: reuse existing user_id [UC-ONB-05]
    end
    UC->>SES: status = Validated, validated_at = now, validated_by = officer_id
    Note over SES: session now terminal + immutable — the provisioning record
    UC-->>BUS: emit OnboardingValidated { onboarding_id }
    Note over UC,BUS: future steps (account, subscription, first invoice)\nappend here, each idempotent, in provision-before-charge order

Design rule — every step is idempotent. Each saga step is safe to retry with no duplicate effect ([UC-ONB-27], [UC-ONB-05] “create only what doesn’t exist”). This round the freeze is idempotent because Validated is terminal: re-validating a frozen session is a no-op that returns it unchanged.

Design rule — the saga is resumable and converges. On partial failure the saga resumes from the last incomplete step and converges, or surfaces a partial-provisioning dead-end to the back office for manual recovery ([UC-ONB-27], [UC-BO-08]).

Design rule — provision-before-charge ordering (future steps). When billing steps are added, the order is fixed: create the account (+ credit any pending top-up) before issuing/collecting the first invoice ([UC-J-01], [UC-ONB-27]). This round writes no billing, so the ordering is documented for forward compatibility.

⚠️ Open item. The compensation / rollback policy when a downstream saga step is unrecoverable is unresolved — tracked in uncertainties ONB-U4.

4. Partial rejection (request_changes)

Design rule — request_changes marks Steps unmet + attaches an OfficerComment; the resolver re-surfaces them. An officer who invalidates specific Step(s) does not discard captured data: the named Step(s) are added to the session’s unmet_steps, an OfficerComment is appended (free text + the affected Step ids

5. Full rejection (AML / fraud)

A full rejection ([UC-ONB-07]) moves the session to Rejected (terminal); no account is provisioned and the session is never frozen as Validated. Downstream AML obligations are owned by compliance, out of scope here. Distinct from a KYC provider failure ([UC-ONB-14]), which leaves the id_doc_pvid Step unmet rather than rejecting the onboarding.

6. Related documents

6. Integrations & Adapters

Integrations & Adapters

This document defines the external integrations the sole-trader Steps depend on: the INSEE SIRET lookup, the OTP sender, and the identity-verification (PVID) provider. Adapters live in infrastructure/adapters/; Steps depend on the trait, not the concrete client, and receive them through StepContext (2. Engine §2).

1. Adapter principles

Design rule — Steps depend on traits, adapters wrap clients. Each Step calls a narrow trait (SiretLookupProvider, EmailSender, PhoneVerifier, IdentityVerificationProvider). The concrete implementation lives in infrastructure/adapters/; tests inject a mock. This keeps the engine pure and the wire concerns isolated.

Design rule — reuse existing clients before adding new ones. Two of the three adapters wrap clients that already exist in the repo (INSEE, messaging). Only the PVID provider introduces a genuinely new external dependency — Ubble (Checkout.com), now wrapped by the new clients::checkout transport and the commercial_domain/checkout_pvid domain crate.

2. SIRET lookup — SiretLookupProvider (INSEE/Sirene)

Used by the siret_lookup Step (3 §2.2).

Design rule — reuse clients::insee. SiretLookupProvider wraps the existing clients::insee::InseeApi::get_company_data(siret) trait method. It maps the registry response to the bag DataPoints LegalName, LegalForm, CompanyAddress, and ApeCode. No new client is built.

3. Contact verification — EmailSender + PhoneVerifier

Used by the get_email and get_phone Steps (3 §2.1/§2.5).

Design rule — the code is self-issued; the adapter only sends. Each Step generates the one-time code and stores its hash + expiry in the DataBag; the adapter only delivers the plaintext code. Verification is a local hash compare within the expiry window — no provider round-trip to verify.

Decided — self-issued. Green-Got generates and verifies the OTP itself (hash + expiry in the bag, local compare); the provider’s role is delivery only, for both get_email and get_phone. The OTP secret stays on our side and failure / abuse-rate-limit handling is ours. Only the delivery transports remain stubbed/deferred ([UC-ONB-33]).

4. Identity verification — IdentityVerificationProvider (PVID)

Used by the id_doc_pvid Step (3 §2.3).

Design rule — a trait with a real impl and a config-gated sandbox. The Step depends on the IdentityVerificationProvider trait. The real provider is Ubble — acquired by Checkout.com, now branded “Checkout.com – Identity Verification” — using Ubble’s Certified / PVID configuration (the ANSSI / French-state-certified remote-IDV scheme required for a French banking licence). The full vendor contract lives in the Checkout (Ubble) PVID integration doc. There are two implementations:

pub trait IdentityVerificationProvider: Send + Sync {
    async fn start_check(&self, subject: &IdentitySubject) -> Result<CheckHandle, ProviderError>;
    async fn poll(&self, handle: &CheckHandle) -> Result<IdentityVerificationStatus, ProviderError>;
}

The result populates IdentityVerificationStatus; an async result is written back to the bag by a rule/activity (8. Architecture). A successful PVID is non-editable at recap ([UC-ONB-04]).

⚠️ Launch-blocking until the final wire lands — vendor decided, build mostly done. PVID = Ubble (Checkout.com). The integration is largely built: the commercial_domain/checkout_pvid crate (stores, use-cases, status machine), the clients::checkout transport (mTLS, v2 DTOs, Cko-Signature verifier), the checkout_webhook service, back-office routes, the mirror migrations, and the mTLS cert/key + webhook signature keys in src/env/src/parameters/checkout.rs. Still open: the live Ubble Basic credentials (client_id/client_secret, Secret::todo()) and the Certified/PVID user_journey_id (empty) — pending from the Ubble account manager; and the onboarding seam, whose RealIdentityProvider still bail!s — the synchronous verify() must be reshaped to start + webhook-driven completion before checkout_pvid plugs in. Until then only the sandbox path completes identity verification. Remaining work is tracked in the Checkout (Ubble) PVID integration §12–§13.

5. Credentials summary

AdapterClientCredentialsStatus
SiretLookupProviderclients::insee::InseeApiPARAMETERS.insee.api_keyexists
EmailSendernone (email OTP)nonestubbed (no email transport in core yet)
PhoneVerifier (web)messaging / vonageexisting messaging credsstubbed
PhoneVerifier (mobile)Infobip silent-authPARAMETERS.infobip.* (params only)deferred
IdentityVerificationProviderUbble (Checkout.com)clients::checkout + checkout_pvidmTLS cert/key + signature keys set in parameters/checkout.rs; Basic client_id/client_secret + user_journey_id pendingmostly built, launch-blocking until creds + onboarding wire land (PVID integration §12)

6. Related documents

7. Data Model

Data Model

This document defines the persisted onboarding entities — OnboardingSession (which, once Validated, is itself the immutable provisioning record) and the OfficerComment embedded on it — their fields, and the ER relationships. Ids are prefixed time_sortable_id strings.

1. Entity-relationship diagram

erDiagram
    OFFER ||--o{ ONBOARDING_SESSION : "chosen for"
    USER  ||--o{ ONBOARDING_SESSION : "owns when authenticated (≤1 in-flight)"
    PROSPECT_TOKEN ||--o{ ONBOARDING_SESSION : "owns when anonymous (≤1 in-flight)"
    ONBOARDING_SESSION ||--o{ OFFICER_COMMENT : "embeds (jsonb)"
    ONBOARDING_SESSION ||--o{ ONBOARDING_EVENT : "records (append-only)"
    DEVICE ||--o{ ONBOARDING_EVENT : "seen in (server meta)"

    ONBOARDING_SESSION {
        text id PK "onb_…"
        text offer_id FK "offers crate"
        enum segment "Retail|Business (denormalised from the offer)"
        text user_id FK "authenticated owner; nullable (XOR prospect_token)"
        text prospect_token "anonymous owner; nullable (XOR user_id)"
        enum status "Draft|InProgress|Submitted|UnderReview|Validated|ChangesRequested|Abandoned|Expired"
        jsonb data_bag "DataPoint -> BagValue"
        text_array required_steps "snapshot of offer_required_step"
        text_array unmet_steps "officer remediation"
        jsonb officer_comments "OfficerComment[]"
        text current_cursor "last resolver Step"
        timestamptz started_at
        timestamptz last_active_at
        timestamptz submitted_at
        timestamptz validated_at "validation freeze stamp"
        text validated_by "validating officer operator id"
        timestamptz expires_at "TTL from offer"
    }
    OFFICER_COMMENT {
        text comment_id
        text_array steps "affected StepIds"
        text reason_code
        text body
        text officer_id
        timestamptz created_at
    }
    ONBOARDING_EVENT {
        text id PK "onbev_…"
        text onboarding_id FK "onboarding_session"
        bigint seq "per-onboarding monotonic order"
        text kind "step_submitted | …"
        timestamptz occurred_at
        text actor "prospect | user_id | officer_id"
        jsonb server_meta "ip, geo, country, asn, device_id, device_type, user_agent, step_code, saved_data_points[]"
        jsonb client_meta "decoded x-onb-meta: view, duration_ms, tab_switches, paste_count, copy_count, …"
    }

OFFER and USER are referenced, not owned here (offers, and the identity domain respectively); PROSPECT_TOKEN is the opaque server-issued cookie handle for an anonymous owner ([UC-ONB-28]) — minted at create_onboarding and retired at get_email, where the owner is promoted to a real user_id ([UC-ONB-31]) — not a stored aggregate. See 1. Overview §3.

2. OnboardingSession

The aggregate root. Carries both the resolver inputs (required_steps, data_bag, unmet_steps) and the lifecycle state (status + timestamps).

FieldTypeNotes
idonb_…Prefixed, time-sortable.
offer_idtextThe chosen offer (owned by offers). One offer per onboarding ([UC-ONB-02]).
segmentSegment (Retail|Business)Denormalised from the chosen offer’s segment ([UC-OFF-05]) at create. Lets the back-office business vs retail validation queues filter and paginate at the DB level ([UC-BO-13]); offers are code-defined and not joinable in SQL. Invariant across a confirm_offer sibling switch — a variant group is single-segment ([UC-ONB-34]).
user_idtext, nullableAuthenticated owner. Exactly one of user_id / prospect_token is set ([UC-ONB-28]). ≤ 1 in-flight per user ([UC-ONB-03]).
prospect_tokentext, nullableAnonymous owner — the opaque server-issued cookie handle ([UC-ONB-28]), minted at create_onboarding with an empty bag. ≤ 1 in-flight per token. The get_email Step flips an anonymous row to user_id once the email is verified ([UC-ONB-29]/[UC-ONB-31]); sign-in mid-flow claims it instead ([UC-ONB-30]).
statusOnboardingStatusSee 4. Lifecycle.
data_bagjsonbDataPoint → BagValue; transient working state ([UC-ONB-26]).
required_stepstext[]Snapshot of the offer’s offer_required_step set at create; immutable for the session ([UC-ONB-02]/[UC-ONB-23]).
unmet_stepstext[]Officer-injected remediation; re-surfaced by the resolver ([UC-ONB-06]).
officer_commentsjsonbOfficerComment[] (§4).
current_cursortext, nullableLast resolver output, for quick resume ([UC-ONB-03]).
started_at, last_active_attimestamptzlast_active_at doubles as updated-at.
submitted_at, validated_attimestamptz, nullableLifecycle stamps; validated_at is the validation-freeze time.
validated_bytext, nullableOperator id of the validating compliance officer. Some iff Validated ([UC-ONB-05]).
expires_attimestamptzTTL from the offer; drives archival ([UC-ONB-15]).

Invariant — exactly one owner. Each session is owned by exactly one of user_id (authenticated) or prospect_token (anonymous), fixed at create and flipped only by promotion/claim ([UC-ONB-28]/[UC-ONB-29]/[UC-ONB-30]). Enforced by a CHECK:

ALTER TABLE onboarding_session
    ADD CONSTRAINT onboarding_session_one_owner_chk
    CHECK ((user_id IS NOT NULL) <> (prospect_token IS NOT NULL));

Invariant — at most one in-flight onboarding per owner. Enforced by a partial unique index on user_id and a parallel one on prospect_token, each restricted to non-terminal statuses ([UC-ONB-03]/[UC-ONB-28]):

CREATE UNIQUE INDEX onboarding_session_one_in_flight_user_idx
    ON onboarding_session (user_id)
    WHERE user_id IS NOT NULL
      AND status IN ('Draft','InProgress','Submitted','UnderReview','ChangesRequested');

CREATE UNIQUE INDEX onboarding_session_one_in_flight_prospect_idx
    ON onboarding_session (prospect_token)
    WHERE prospect_token IS NOT NULL
      AND status IN ('Draft','InProgress','Submitted','UnderReview','ChangesRequested');

Design rule — duplicate-identity check at each contact Step ([UC-ONB-32]). The email is checked at get_email and the phone at get_phone ([UC-ONB-31]) for uniqueness against existing login-identifiers. The authoritative user/account store is a referenced domain (not a column here), so this is a lookup against canonical identity, not a constraint on onboarding_session: a match short-circuits to “log in” with no user/identifier/session written ([UC-ONB-32]). This keeps the one-account-per-identity rule at the entry while onboarding_session itself stores no real identity until promotion ([UC-ONB-29]).

Design rule — the DataBag is JSONB, typed in Rust. One row per session, atomic bag updates inside a transaction, serde round-trip via sqlx::types::Json<DataBag>. The schema is the DataPoint enum, not DB columns (2. Engine §4).

Design rule — required_steps is a snapshot, never a live join. Storing the resolved Step ids (rather than re-reading the offer each request) is what makes the in-flight requirement set immutable and lets the resolver be a pure function ([UC-ONB-23], 2. Engine §5).

3. The validation freeze (no separate provisioning row)

There is no ProvisionedCustomer table. Validation is a freeze of the session itself (5. Validation §2): the status flips to Validated and the validated_at / validated_by columns above are stamped on the session row. From then the session is terminal and immutable — its frozen data_bag is the snapshot of what was approved, and the session row is the durable record that provisioning happened.

Invariant — validation is idempotent. Validated is terminal, so re-validating is a no-op that returns the frozen session unchanged ([UC-ONB-27], [UC-ONB-05]) — no duplicate write, no separate idempotency key needed.

Design rule — no real cross-domain rows this round. Validation does not write physical_person, organisation, or bank_account aggregates ([UC-ONB-05]). Those are added when real provisioning is wired; a dedicated provisioning record is introduced only if/when that multi-step saga needs an identity separate from the session.

4. OfficerComment (embedded)

Embedded JSONB on the session, appended by request_changes (5. Validation §4).

FieldTypeNotes
comment_idtextStable id within the array.
stepstext[]The Step ids this comment marks unmet ([UC-ONB-06]).
reason_codetextMandatory BO reason code ([UC-BO-10]).
bodytextFree-text officer note shown to the customer.
officer_idtextThe acting officer (audit).
created_attimestamptz

Design rule — comments and unmet_steps move together. Appending an OfficerComment always accompanies adding its steps to unmet_steps, so the resolver re-surfaces exactly the commented Steps ([UC-ONB-06]).

5. OnboardingEvent (append-only risk-signal log)

A dedicated onboarding_event table — not the DataBag — records the risk/fraud signal timeline ([UC-ONB-35]). It is append-only (never updated or deleted outside retention purge) so it is a trustworthy audit trail ([UC-BO-10]). One row per recorded interaction (today: each step submission), ordered per onboarding by a monotonic seq.

FieldTypeNotes
idonbev_…Prefixed, time-sortable.
onboarding_idtextFK to onboarding_session. Indexed for the back-office read.
seqbigintPer-onboarding monotonic order (max(seq)+1 at append).
kindtextEvent kind — step_submitted this round; the enum grows (e.g. step_failed, resumed).
occurred_attimestamptzWhen the event happened.
actortextWho acted — prospect:<token-hash>, a user_id, or an officer_id.
server_metajsonbAuthoritative, backend-captured: ip, city/region/country, latitude/longitude, asn (from the CDN edge, mirroring AuthRequestContext), device_id + device_type, user_agent, step_code, and saved_data_points (the DataPoint keys written — never raw regulated values, never OTP hashes / CHD / SAD, [UC-REG-08]).
client_metajsonbUntrusted, decoded from the obfuscated x-onb-meta header: view, duration_ms, tab_switches, paste_count, copy_count, focus/blur. Advisory only; may be absent/forged.

Design rule — global signals are derived, not stored. The back-office “global signals” ([UC-BO-12]) — total duration, device list, device-changed, cross-onboarding device-reuse + links, IP/browser/country arrays — are computed at read time by aggregating this table, not persisted in a separate summary row. Device-reuse is a query for other onboarding_ids whose events share a server_meta->>'device_id' (that key is indexed). This keeps the write path a single append and avoids a summary that can drift.

Design rule — the event log never holds the source of truth. It records that a DataPoint was written and the context around it; the value itself lives in the session DataBag (and, post-validation, the canonical record). This is what lets archival hard-delete the transient identity bootstrap ([UC-ONB-15]) without gutting the fraud trail — the kept events reference keys and context, not the throwaway login’s secrets.

6. Retention

Design rule — sessions are archived, not deleted, on expiry/abandon. Expired or abandoned sessions are retained for audit and follow the purpose-based retention / anonymisation model ([UC-ONB-15]/[UC-REG-08]); the DataBag is transient working state, but the row persists until the retention policy purges/anonymises it. A Validated session is terminal and retained — it is the provisioning record, so its frozen data_bag snapshot persists with it.

7. Related documents

8. Architecture

Architecture

This document defines the crate’s DDD layering, the stores, the use cases (customer and back-office), the eventbus/Temporal surfaces, and the API surface across business_api and back_office_api.

1. Layers

src/commercial_domain/onboarding/src/
├── lib.rs
├── service.rs            # OnboardingService impl Service; STEP_REGISTRY coverage assertion
├── parameters.rs         # adapter creds (insee reused; PVID new; OTP via messaging)
├── definitions.rs        # OnboardingEvent enum + EVENT_BUS
├── domain/               # Step trait, registry, resolver, DataBag, session, OfficerComment
├── use_cases/            # one file per command/query (§3)
├── stores/               # session_store, models
├── infrastructure/adapters/   # siret_lookup, otp_sender, identity_verification (§ docs/6)
├── steps/                # the six sole-trader Steps (docs/3); feeds STEP_REGISTRY
├── workflows/            # daily expiry sweep (TTL archival)
├── activities/           # stateful: db_pool; expiry batch; async identity-result write-back
└── rules/                # on_identity_result (async PVID callback → bag)

Design rule — the domain layer is pure. domain/ (the Step trait, the resolver, DataBag) has no IO; the resolver is a pure function unit-tested exhaustively (2. Engine §5). IO lives in stores/, infrastructure/, activities/. This mirrors the architecture skill and the sibling invoicing crate (adapters + domain + eventbus + Temporal in one crate).

Design rule — the crate is the cross-domain seam. Unlike the segment-neutral catalogue crates, onboarding depends on the domains it orchestrates — this round on offers (for the Step snapshot) and the clients/messaging adapter crates; real physical_person/organisation/core_banking dependencies are added when real provisioning lands ([UC-ONB-13], Appendix A).

2. Stores

StoreKey methods
session_storecreate, find_by_id, find_in_flight_for_owner (one-in-flight guard, by user_id or prospect_token — [UC-ONB-28]), update_bag, update_status, mark_validated (freeze: status + validated_at + validated_by — [UC-ONB-05]), promote_to_user (prospect_token → user_id on validation/claim — [UC-ONB-29]/[UC-ONB-30]), set_unmet_steps, add_officer_comment, list_for_back_office, list_awaiting_validation

Migrations create the single onboarding_session table with the partial-unique one-in-flight indexes and the validation-freeze columns (validated_at, validated_by) — there is no separate provisioned-customer table (7. Data Model).

3. Use cases

Customer-facing (called from business_api):

Use caseEffect
create_onboardingbind owner (issue prospect-token cookie if anonymous, else user_id); one-in-flight guard per owner; snapshot offer_required_step; pre-fill bag (authenticated) or empty (anonymous); → Draft ([UC-ONB-02]/[UC-ONB-28])
get_current_steprun resolver; return next StepId + prefill + status ([UC-ONB-03])
submit_stepvalidate, step.on_complete, persist bag, resolve next ([UC-ONB-02])
submit_for_reviewguard resolver == None; → Submitted
abandon_onboardingAbandoned
get_data_bagrate-limited, redacted preview

Back-office (called from back_office_api, [UC-ONB-05]/[UC-BO-06]/[UC-BO-07]):

Use caseEffect
validate_onboardingguard Submitted/UnderReview + no gap; promote anonymous owner; freeze session (stamp validated_at + validated_by); → Validated ([UC-ONB-05]/[UC-ONB-27])
request_changesmark Step(s) unmet + OfficerComment; → ChangesRequested ([UC-ONB-06])
reject_onboardingfull AML/fraud reject; → Rejected ([UC-ONB-07])

Design rule — every BO action is audited with a reason code and is maker-checker. BO writes carry an actor, timestamp, reason code, and before/after ([UC-BO-10]). Four-eyes (manager) approval is scoped to manual data modifications/overrides ([UC-BO-09], not implemented in this slice) — a plain validation of a complete submission does not require co-approval. The BO cannot override hard regulatory constraints (sanctions, KYC/KYB minimums, etc.) ([UC-BO-09]).

4. Events & Temporal

definitions.rs declares OnboardingEvent and EVENT_BUS (plan.md EventBus contract). OnboardingValidated is the seam downstream commercial crates (subscriptions, billing) subscribe to once real provisioning is wired.

5. API surface

retail_api (mobile) and business_api (web) expose the same onboarding operations (RPC-style — every call is a POST at /{surface}/onboarding/{op}); the offer segment is fixed by the surface (retail → Retail, business → Business), never a request field ([UC-ONB-01]). Both serve both owner kinds ([UC-ONB-28]):

POST /{retail|business}/onboarding/list_offers        startable offers for { country[, legalForm] }
POST /{retail|business}/onboarding/start_onboarding    anonymous create-first entry → { prospectToken, onboarding }
POST /{retail|business}/onboarding/create_onboarding   authenticated in-app entry (existing customer, another offer)
POST /{retail|business}/onboarding/get_onboarding      resume → current state, or the latest terminal Rejected
POST /{retail|business}/onboarding/submit_step         { onboardingId, step: { stepCode, … } }
POST /{retail|business}/onboarding/sibling_offers      the confirm_offer choices (current + startable siblings)
POST /{retail|business}/onboarding/get_recap           recap groups (B2B; retail has no recap step)
POST /{retail|business}/onboarding/edit_step           reopen a completed step
POST /{retail|business}/onboarding/submit_for_review    finish → PendingValidation

On retail, start_onboarding needs no body (offerId defaults to the free offer, country to FR); business requires both. The wire format is camelCase at the boundary. The mobile contract is 10. Retail Mobile Integration.

Design rule — the customer endpoints serve anonymous and authenticated callers. The same routes are reachable publicly by an anonymous prospect — the backend establishes an opaque, httpOnly prospect-token cookie at create_onboarding, then upgrades it to a real authenticated session at the get_email Step (see the unauthenticated-entry rule below) and reads whichever applies on every subsequent call — and by an authenticated caller via the session user_id ([UC-ONB-28]). From inside the app POST /onboarding starts an authenticated session pre-filled from canonical KYC ([UC-ONB-10]). The same Step registry, resolver, and validation serve both; only the identity binding and pre-fill differ.

Design rule — the unauthenticated entry: create_onboarding + the public get_email submit. On the logged-out path the calls reachable before a real session exists are create_onboarding (which mints the prospect-token cookie) and the get_email submit (3. Step Dictionary §2.1). On verifying the email OTP, get_email creates the user + verified identifier and mints the authenticated session (the standard httpOnly session cookie/token), promoting the owner prospect→user_id ([UC-ONB-31]) — but only after a duplicate-identity check on the email ([UC-ONB-32]): a match writes nothing and returns “this credential already exists — please log in”, routing the prospect to login. get_phone runs the same per-step dedup on the phone. Every other customer endpoint requires the prospect session or the authenticated session and authorises caller-to-owner.

Design rule — every customer call authorises caller-to-owner; mismatch is not-found. Each read/resume/submit/edit/submit-for-review matches the caller’s identity (prospect-token cookie or user_id) to the session owner; a mismatch is treated as not found (no existence disclosure) ([UC-ONB-28]). The subscribable-offers gate is unauthenticated ([UC-ONB-01]). At validation an anonymous session is promoted to a real user ([UC-ONB-29]); a mid-flow sign-in claims it ([UC-ONB-30], 4. Lifecycle §2.7).

back_office_api (operator; auth-separated; audited — unchanged by the owner model: officer endpoints always act on an existing session by id):

GET    /backoffice/onboardings             list (filter status/offer/dates)
GET    /backoffice/onboardings/{id}        full session + comments + resolver preview
POST   /backoffice/onboardings/{id}/validate          validate_onboarding
POST   /backoffice/onboardings/{id}/request-changes   request_changes { steps, reason_code, body }
POST   /backoffice/onboardings/{id}/reject            reject_onboarding { reason_code }

Design rule — the FE renders the backend-named Step; it never decides the next Step. GET /onboarding/{id}/current is the single source of “what to render”; the FE picks presentation only ([UC-ONB-03]). Deep-link resume: /{onboardingId}/{onboardingId}?step=<next-missing>.

6. Related documents

9. Onboarding Events & Risk Signals

Onboarding Events & Risk Signals

This document specifies the onboarding event log ([UC-ONB-35]), the PEP self-declaration Step ([UC-ONB-36]), and the back-office risk & signals panel ([UC-BO-12]) that surfaces both to compliance and fraud officers. It is the concrete realisation of the per-step fraud signals reserved in [UC-ONB-08]. It complements 7. Data Model (the onboarding_event table) and 3. Step Dictionary (the pep_declaration Step).

1. Purpose

Compliance and fraud need to reconstruct what happened during an onboarding — when each piece of data was submitted, from what IP, on what device, from which country, and how the person behaved on each screen — so they can validate the onboarding with confidence and, eventually, score it. This build delivers the capture + human-readable surfacing; the automated score is the future goal and is out of scope here. Nothing in the log gates a decision automatically today.

2. The event model

Every meaningful interaction appends one immutable row to onboarding_event (7. Data Model §5). This round emits a single kind, step_submitted, on each step submission; the kind enum is designed to grow (step_failed, resumed, officer_viewed) without a schema change. Rows are append-only: the log is never mutated or deleted except by the retention purge, which is what makes it a trustworthy audit trail ([UC-BO-10]).

Each event carries two independently-sourced meta blobs.

2.1 Server meta — authoritative, never client-trusted

Captured entirely backend-side while handling the step submission, so it cannot be forged by the client:

FieldSource
ip, city, region, country, latitude, longitude, asnthe Cdn axum extractor (utils::axum::extractors::cdn) → the same shape the authentication domain records as AuthRequestContext. CloudFront edge headers in prod; ConnectInfo + a dev geolocation fetch locally.
device_id, device_typethe existing device domain — web = the device_id httpOnly cookie, mobile = the x-device-id header (authentication::…::extract::extract_device_info).
user_agentrequest header (bounded length).
step_codethe step being submitted.
saved_data_pointsthe keys (DataPoint names) the step wrote — not the values.

Rule — no regulated values, no secrets in the log. saved_data_points records that e.g. PersonalLastName was written, not its value; the value stays in the DataBag / canonical record. OTP challenge hashes, any CHD/SAD, and other secret markers are never serialised into an event ([UC-REG-08]). This keeps the archival hard-delete of the transient identity bootstrap ([UC-ONB-15]) meaningful — the retained fraud trail references keys and context, not the throwaway login’s secrets.

2.2 Client meta — behavioural, obfuscated, untrusted

The front end instruments each onboarding view and accumulates per-view behavioural signals, then packs them into an obfuscated header sent on every step submission:

Backend handling. The business-api step handler reads x-onb-meta, base64-decodes and JSON-parses it defensively (a malformed/oversized header is dropped, not fatal — the step still advances), and hands the decoded value to the event append. Size is bounded; parse failures are swallowed to a None.

3. Write path

The append happens inside the same transaction as the step’s DataBag write in the submit_step use case, so an event exists iff the step persisted — no orphan events, no silent gaps. seq is max(seq)+1 for the onboarding, computed in-transaction. The store exposes an append-only record; the domain never updates or deletes an event. (A later mirror to the analytics warehouse — ClickHouse via the event bus — is additive and does not change this Postgres contract; it is the scoring seam.)

4. PEP self-declaration Step

pep_declaration ([UC-ONB-36], 3. Step Dictionary §2.6) captures politically-exposed-person status as a regulated self-declaration feeding the ongoing PEP/adverse-media compliance gate ([UC-REG-02]). It writes:

It is pure validation (no adapter), requires PersonalLastName so the declared person is identified, and is required by every offer (retail and sole-trader) — placed immediately after identity verification (id_doc_pvid), alongside the other regulated Steps. A positive declaration does not block the Step or auto-reject the onboarding; it is surfaced to the officer ([UC-BO-12]) for the enhanced-due-diligence decision at validation ([UC-ONB-05]). Screening the declared identity against sanctions/PEP watchlists (OFAC, UN, EU) is a separate regime ([UC-REG-02]) and is out of scope here.

5. Back-office risk & signals panel

The onboarding detail page ([UC-BO-12]) gains four sections, all fed by the read use case get_onboarding_signals (events + derived aggregates + the PEP DataPoints):

  1. Onboarding events — the raw append-only timeline: per event, timestamp, step, DataPoints written, decoded server + client meta. Read-only.
  2. PEP — the four declaration fields above.
  3. Screening (out of scope) — a static placeholder marking sanctions/watchlist screening as a not-yet-wired separate regime ([UC-REG-02]).
  4. Global signals (derived) — see §6.

6. Derived global signals

Computed by aggregating the onboarding’s events at read time — not stored:

SignalDerivation
Total durationlast event occurred_at − started_at (or first event).
Devices useddistinct server_meta.device_id (+ type) across events.
Device changedmore than one distinct device_id.
Device reuse + linksfor each device_id, other onboarding_ids with an event sharing it (indexed lookup on server_meta->>'device_id'); returned as links for the officer.
IP arraydistinct server_meta.ip.
Browser arraydistinct server_meta.user_agent.
Country array + flagsdistinct server_meta.country (ISO code → flag rendered by the front end).
Behavioural roll-upsums/maxes of client_meta counters (paste, tab-switches, dwell) — advisory.

Everything is an array because a legitimate onboarding can span multiple devices, IPs, and countries ([UC-ONB-17] cross-device resume). Device-reuse across onboardings is the only cross-onboarding query; it uses the indexed device_id key in server_meta.

7. Retention & privacy

Events follow the session’s retention/anonymisation model ([UC-ONB-15]/[UC-REG-08]): retained for audit, purged/anonymised on the same purpose-based schedule. Because the log stores DataPoint keys and request context — not regulated values or secrets — right-to-erasure of the identity bootstrap and the KYC-retention obligation both remain satisfiable without special-casing the event log.

8. Related documents

Uncertainties

Onboarding — Open Items

This is the active register of what is still open in the onboarding domain — the items that still need a decision or research before they can be closed. The cross-segment model itself is settled and documented in the numbered docs (offer-driven Step snapshot, the resolver, the status lifecycle, the dual-mode owner, onboarding-domain-real provisioning, the adapter set).

Scope of this doc. It lists only unresolved items. Once an item is decided its outcome is recorded in the canonical doc that owns it (see Resolved elsewhere below) and removed from here — this register stays a short, honest list of remaining work, not a decision log.

Convention. “Launch-blocking = yes” means a real sole-trader customer cannot be onboarded end-to-end until the item is closed.


Open items

ONB-U8 — UC-ONB-32 duplicate-identity account-enumeration message

Question: At each contact Step, when the submitted email (get_email) or phone (get_phone) already belongs to an existing login-identifier, the backend short-circuits to “log in” instead of creating the user/session ([UC-ONB-32]). What exactly should the response say? A precise “this email/phone already exists” message leaks account existence (enumeration); consider a softer “if you already have an account, log in” message plus an email/SMS notification to the real owner. The decision — and whether to add the out-of-band notification — is the open trade-off; the early-exit behaviour itself is decided.

Related documents

Subscriptions

0. Documentation Index

Subscriptions — Documentation Index

The subscriptions crate owns the living holder↔offer relation: the Subscription aggregate, its lifecycle, offer changes, bundles, suspension, and async closure wind-down. It is segment-neutral data + logic that references banking, identity, and investment objects but owns none of them. The catalogue (offers / modules / steps) lives in offers; the money of a change (proration, netting, refunds) is posted by customer_billing; the banking account / IBAN / ledger lives in core_banking. These documents are the source of truth for the subscription domain; the cross-domain spine is commercial_domain/docs/0_use_cases.md (UC-SUB-*).

Overview & model

Lifecycle & changes

Architecture & open items

To Be Created

1. Subscriptions Overview

Subscriptions Overview

This document fixes the scope of the subscriptions crate, the shape of the holder↔offer relation, the load-bearing invariants, and the boundaries with the catalogue, the billing subledger, and the banking account.

1. What a Subscription is

A Subscription is the living relation between a holder and an offer. It snapshots the offer_version, each referenced module_version, and the price at creation; it carries dates, a billing cadence, a status, customer-specific modifiers, and a filiation link to its predecessor (UC-SUB-32). A single subscription may cover several accounts/modules (a bundle) at one price (UC-SUB-04).

The canonical shapes:

2. Domain boundaries — owns vs references

Design rule — segment-neutral; owns the relation, not the objects it relates. subscriptions records and drives subscription/lifecycle state. It references banking, identity, and investment objects and owns none of them (UC-SUB-12, Appendix A).

OwnsReferences (owned elsewhere)
The Subscription aggregate + its three-lifecycle state (UC-SUB-33)the banking account / IBAN / ledger (core_banking)
Offer changes (close-then-open), bundles, suspension, closure wind-downthe Offer / Module / Step catalogue, eligibility, versioning (offers)
The offer/module snapshot + customer modifier declarationsidentity / KYC (physical_person); AML decisions (compliance)
Filiation lineage (previous_subscription_id)savings / ASV / PER contracts (investment + partners)
Lifecycle events that trigger billinginvoices / proration / refunds / modifier application (customer_billing); revenue recognition / VAT (accounting)

Design rule — the money lives in customer_billing. The amount of a change (daily pro-rata, the netted upgrade invoice, the credit line, a pro-rata refund) is computed and posted by customer_billing on the events this crate emits (UC-BIL-08, UC-BIL-09). Modifiers are read at each billing cycle and applied at the invoicing layer, never by mutating the subscription (UC-SUB-29).

3. Load-bearing invariants

Invariant — one account (active IBAN) ↔ exactly one active subscription. An IBAN exists in only one subscription. A bundle subscription can cover several accounts/modules, but each of those accounts still belongs to that single subscription (UC-SUB-12). Therefore changing what an account is subscribed to is never a second subscription on the same account — it is an offer change (close-then-open) on that account’s subscription (UC-SUB-06 / UC-SUB-07). A genuinely different product (shared, livret, ASV) is a separate account → separate subscription.

Invariant — every change is close-then-open with filiation. The contractual basis (price, module composition, entitlements) is immutable/versioned and never edited in place. A change expires the old subscription and creates a new one, linked by previous_subscription_id (UC-SUB-12, UC-SUB-32, Appendix A). This is what makes grandfathering (UC-SUB-17) and clean migration work.

Invariant — a live subscription is never mutated by catalogue/price changes. The snapshot freezes what the subscription references at creation; later catalogue edits and price changes do not touch it (UC-SUB-32).

Invariant — three lifecycles never collapse into one. Offer, subscription, and account are separate state machines: a subscription ending does not instantly close the account; a retired offer does not close live subscriptions; suspension acts on the account, not the subscription’s commercial state (UC-SUB-33). See 3. Lifecycles and State Machines.

Invariant — suspension is AML-only. An unpaid fee never suspends; it produces a commercial arrears state instead (UC-SUB-14, UC-SUB-22). See 6. Suspension, Closure and Death.

4. Time vocabulary (UC-SUB-31)

Defined once here and used throughout:

Behaviours keyed to the boundary: continuous-eligibility re-check (UC-OFF-04), downgrade effect (UC-SUB-07), commercial cancellation (UC-SUB-23), bundle-merge (UC-SUB-21), and modifier read (UC-SUB-29). One-off fees are point-in-time and not tied to the period (UC-BIL-04).

5. Related Documents

2. Data Model

Data Model

This document defines the Subscription aggregate — its fields, the immutable offer/module snapshot, the customer-specific modifiers applied at the invoicing layer, and the filiation lineage. Money is integer cents (i64); ids are prefixed time_sortable_id strings.

1. The Subscription aggregate

A Subscription is the living holder↔offer relation. It freezes what it references at creation and links its predecessor by filiation.

FieldTypeNotes
id“sub_…”Prefixed, time-sortable.
holder_idstringReference to the holder (physical_person / organisation); the holder owns the account + money (UC-ONB-12).
account_idsVec<string>The account(s) this subscription covers; ≥1, several for a bundle (UC-SUB-04). Each active IBAN belongs to exactly one active subscription (UC-SUB-12).
offer_snapshotOfferSnapshotFrozen offer_version + module_versions + price_snapshot (UC-SUB-32). See §2.
cadenceMonthly | AnnualBilling cadence (UC-SUB-31).
anchortimestampThe anniversary anchor; drifts to the switch date on an upgrade (UC-BIL-08).
period[start, end)The span the current charge covers (UC-SUB-31).
statusSubscriptionStatusSee 3. Lifecycles (UC-SUB-33).
modifiers[]SubscriptionModifierCustomer-specific pricing overrides, applied at invoicing (UC-SUB-29). See §3.
previous_subscription_id“sub_…”, nullableFiliation to the predecessor a close-then-open created (UC-SUB-32). See §4.
pending_changePendingChange, nullableAt most one, and only a downgrade (UC-SUB-10). See 4. Offer Changes.
created_at, expired_attimestampsexpired_at set when a close-then-open supersedes this generation.

Invariant — one active subscription per active IBAN. Enforced as a partial unique index over account_ids for status = Active (and the change-in-flight states): an IBAN appears in exactly one active subscription (UC-SUB-12).

Design rule — the aggregate holds references, not the referenced objects. The account (IBAN/ledger), the holder identity, and the investment contract are owned by other crates; the subscription stores ids only (UC-SUB-12, UC-SUB-15, Appendix A).

2. The offer snapshot (immutability contract)

Design rule — a subscription freezes what it references at creation. Later catalogue/price changes never mutate a live subscription (UC-SUB-32).

FieldTypeNotes
offer_idstringThe offer the subscription is on.
offer_versionu32The frozen immutable offer version (UC-OFF-06).
module_versions[]ModuleVersionEach referenced module’s frozen version (payment account, shared account, card sub-module, livret, ASV, PER).
price_snapshoti64 (cents)The catalogue price delivered (UC-OFF-03); promos are intrinsic to the offer, not stacked at runtime (Appendix A).
fee_schedule_versionu32The key contract terms / fee-schedule version delivered.

Invariant — the snapshot is never edited; modifiers ride on top. Customer-specific modifiers (§3) are applied at the invoicing layer on top of the snapshot, never by mutating it — keeping offers fixed-price while still allowing per-customer pricing (UC-SUB-29, UC-OFF-06).

3. Customer-specific modifiers

Design rule — modifiers live on the subscription/customer, are read each cycle, and are applied at the invoicing layer. Two holders of the same offer can be billed differently (UC-SUB-29). They never live on the immutable offer.

ModifierEffectSet / changed by
Tag (Free / Employee / …)Zeroes or alters the recurring charge. Set at activation (from a whitelist/code) so a comp applies from the first invoice — no pay-then-reimburse (UC-ONB-09).activation / BO
Free-month counterDecrements at each cycle; increments from parrainage (UC-BIL-20), compensation gestures (UC-BIL-07), or promos (UC-BIL-17).billing events
Promo-code discountA redeemed code attaches a customer-specific discount or free months (UC-BIL-19).code redemption

Invariant — customer_billing applies modifiers; subscriptions only declares them. The subscription stores the modifier set; the per-cycle arithmetic (zeroing, decrementing, discounting) happens at the invoicing layer (UC-SUB-29).

4. Filiation

Design rule — every change is close-then-open, linked by previous_subscription_id. The old subscription is expired and a new one created; the new one’s previous_subscription_id points at the old. The chain is an append-only lineage for audit (UC-SUB-12, UC-SUB-32).

Invariant — filiation is immutable and audit-faithful. A closed generation keeps its snapshot, price, dates, and place in the lineage unchanged; corrections are new generations, never edits of a prior one (UC-SUB-32).

5. Entity-relationship diagram

erDiagram
    HOLDER ||--o{ SUBSCRIPTION : holds
    SUBSCRIPTION ||--|| OFFER_SNAPSHOT : freezes
    SUBSCRIPTION ||--o{ SUBSCRIPTION_MODIFIER : carries
    SUBSCRIPTION ||--o| PENDING_CHANGE : "has at most one (downgrade)"
    SUBSCRIPTION ||--o{ ACCOUNT_LINK : covers
    SUBSCRIPTION ||--o| SUBSCRIPTION : "previous_subscription_id (filiation)"
    OFFER_SNAPSHOT ||--o{ MODULE_VERSION : "snapshots"

    HOLDER {
        string holder_id "ref physical_person / organisation"
    }
    SUBSCRIPTION {
        string id PK "sub_…"
        string holder_id FK
        enum cadence "Monthly | Annual"
        timestamp anchor "drifts on upgrade"
        enum status "Pending|Active|Arrears|Suspended|Expired|Closed"
        string previous_subscription_id FK "nullable, filiation"
        timestamp created_at
        timestamp expired_at "nullable"
    }
    OFFER_SNAPSHOT {
        string offer_id "ref offers"
        u32 offer_version "frozen"
        i64 price_snapshot "cents"
        u32 fee_schedule_version
    }
    MODULE_VERSION {
        string module_id "ref offers catalogue"
        u32 module_version "frozen"
    }
    SUBSCRIPTION_MODIFIER {
        enum kind "Tag | FreeMonthCounter | PromoDiscount"
        string detail "applied at invoicing layer"
    }
    ACCOUNT_LINK {
        string account_id "ref core_banking (IBAN)"
    }
    PENDING_CHANGE {
        string target_offer_id
        timestamp effective_boundary
    }

Note — referenced entities are not owned here. HOLDER, ACCOUNT_LINK (the IBAN), OFFER_SNAPSHOT.offer_id, and the investment contract behind a savings module are owned by physical_person/organisation, core_banking, offers, and investment respectively; the subscription stores references only.

6. Related Documents

3. Lifecycles and State Machines

Lifecycles and State Machines

This document defines the three distinct lifecycles that interact but never collapse into one — offer, subscription, and account — and the canonical subscription status machine (UC-SUB-33).

1. Three distinct lifecycles

Design rule — three separate state machines, never one. Offer, subscription, and account each have their own lifecycle; they interact through events but are persisted and reasoned about independently (UC-SUB-33).

LifecycleOwnerStatesNotes
Offeroffersdraft → active (within [start, end]) → retiredImmutable versions (UC-OFF-06); retirement does not close live subscriptions (UC-SUB-17).
Subscriptionsubscriptionspending → active → (suspended / arrears) → expired/closedAn offer change ends one and opens another (UC-SUB-12).
Accountcore_bankingactive → suspended (AML) / closing → archivedCan outlive its subscription (closing wind-down); tier-agnostic (UC-SUB-16, UC-SUB-19).

Invariant — consequences of separation:

stateDiagram-v2
    direction LR
    state "Offer (offers)" as OfferLife {
        [*] --> Draft
        Draft --> ActiveOffer: publish
        ActiveOffer --> Retired: end date / withdraw
        Retired --> [*]
    }
    state "Subscription (subscriptions)" as SubLife {
        [*] --> Pending
        Pending --> ActiveSub: activation
        ActiveSub --> ExpiredSub: offer change / boundary cancel
        ActiveSub --> ClosedSub: closure
        ExpiredSub --> [*]
        ClosedSub --> [*]
    }
    state "Account (core_banking)" as AcctLife {
        [*] --> ActiveAcct
        ActiveAcct --> SuspendedAcct: AML/fraud
        ActiveAcct --> Closing: closure request
        Closing --> Archived: wind-down settled
        SuspendedAcct --> ActiveAcct: lifted
        SuspendedAcct --> Closing: escalated
        Archived --> [*]
    }

2. The subscription status machine

The subscription’s own status axis. Arrears and Suspended are distinct: Arrears is a commercial unpaid-fee state (UC-SUB-14); Suspended is AML/fraud/security only (UC-SUB-22).

stateDiagram-v2
    [*] --> Pending: open_subscription (snapshot offer/modules + price)
    Pending --> Active: activation provisions accounts [UC-SUB-18]

    Active --> Expired: offer change (close-then-open) [UC-SUB-12]
    Active --> Expired: boundary cancellation [UC-SUB-23]
    Active --> Arrears: fee charge fails — no funds [UC-SUB-14]
    Arrears --> Active: arrears settled
    Arrears --> Expired: persistent failure → write-off + close [UC-BIL-10]

    Active --> Suspended: AML / fraud / security [UC-SUB-22]
    Arrears --> Suspended: AML / fraud / security [UC-SUB-22]
    Suspended --> Active: suspension lifted (privileged) [UC-SUB-22]
    Suspended --> Closed: escalated to closure [UC-SUB-22]

    Active --> Closed: closure request / framework termination [UC-SUB-19/30]
    Active --> Closed: holder death (manual termination) [UC-SUB-27]
    Expired --> Closed: account wind-down completes [UC-SUB-19]

    Closed --> [*]
    Expired --> [*]: superseded generation (filiation kept)

    note right of Suspended
        AML / fraud / security ONLY.
        Payment means blocked; the user can
        still consult/download statements.
        Never an account closure. [UC-SUB-22]
    end note
    note right of Arrears
        Commercial unpaid-fee state.
        Account stays open, subscription stays
        active-but-in-arrears. NOT suspension. [UC-SUB-14]
    end note

Key characteristics:

3. Continuous eligibility → fallback at the boundary

Design rule — commercial eligibility is continuous and re-evaluated every period. If a condition lapses at a period boundary, the subscription transitions to its declared fallback via a scheduled offer change (close-then-open, filiation) — perks kept to the boundary, never a silent mutation, never a commercial refund (UC-OFF-04, UC-OFF-11, Appendix A). Deterministic boundaries (age) are the schedulable/notified sub-case (UC-SUB-20).

stateDiagram-v2
    [*] --> Active
    Active --> BoundaryCheck: period boundary reached
    BoundaryCheck --> Active: eligibility still holds
    BoundaryCheck --> FallbackScheduled: condition lapsed [UC-OFF-04]
    FallbackScheduled --> Active: close-then-open to declared fallback [UC-OFF-11]
    note right of FallbackScheduled
        Perks kept to the boundary; no commercial
        refund; new generation linked by filiation.
    end note

Invariant — every conditional offer resolves to an active fallback. Any non-primitive, force-migratable offer must resolve to an active fallback, verified continuously; primitive offers (e.g. Essential) need none (Appendix A, UC-OFF-11).

4. Related Documents

4. Offer Changes and Proration

Offer Changes and Proration

This document defines how a subscription moves between offers — always close-then-open — how the system classifies a change as upgrade / downgrade / lateral, and how the money of an upgrade is prorated and netted. The amount is computed and posted by customer_billing; subscriptions classifies the move, performs the close-then-open, and emits the trigger (UC-SUB-06/07/08, UC-BIL-08).

1. Offer change = close-then-open

Design rule — changing what an account is subscribed to is an offer change, not a second subscription. Because each active IBAN binds to exactly one active subscription (UC-SUB-12), picking a plan that changes that account’s offer (Essential → Premium) routes to an offer change on the existing subscription: the old subscription is expired and a new one created, linked by previous_subscription_id. A genuinely different product (shared, livret, ASV) is a separate account → separate subscription instead (UC-SUB-12).

Invariant — the contractual basis is immutable/versioned; only the snapshot is re-taken. Price, module composition, and entitlements are never edited in place; the new generation snapshots the new offer_version + module_versions + price (UC-SUB-32, Appendix A). Only non-material presentation fields are editable in place (UC-OFF-06).

2. Classification

stateDiagram-v2
    [*] --> Classify: change_offer(target)
    Classify --> Upgrade: target price > source price
    Classify --> Lateral: target price == source price
    Classify --> Downgrade: target price < source price

    Upgrade --> Collectable: netted invoice ≥ 0 AND balance ≥ amount
    Upgrade --> Declined: balance < amount
    Collectable --> Applied: immediate close-then-open, anchor → switch date
    Declined --> [*]: stay on current offer (no deferred-upgrade state)

    Lateral --> Applied: immediate, treated like upgrade, nothing owed back

    Downgrade --> Scheduled: pending at next boundary, perks kept
    Scheduled --> [*]: cheaper subscription starts next cycle (no refund)

Design rule — the source/target price relation determines the class, and an “upgrade” that nets negative is misclassified. An upgrade’s target is always strictly more expensive than the source, guaranteed at offer-definition time (UC-OFF-09). The system asserts the netted upgrade invoice is ≥ 0 and refuses the move otherwise — a netted-negative “upgrade” is a misclassification, not an upgrade (UC-SUB-06, UC-BIL-08).

3. Upgrade — immediate when collectable, pro-rata

Design rule — upgrades are immediate when collectable, full stop. New perks activate immediately; the delta is billed daily pro-rata on gross (UC-SUB-06, UC-BIL-08). Because fees collect only by internal transfer from the balance and the account is never pushed negative for our own fee (UC-SUB-14), the upgrade is applied only if its netted invoice can be collected at the switch (balance ≥ amount). Otherwise it is declined and the customer stays on the current offer (UC-SUB-06).

Invariant — no free-upgrade and no deferred-upgrade state. Premium perks are never granted ahead of collection; insufficient balance makes the upgrade impossible, not non-immediate. There is no pending/deferred upgrade — only a downgrade can sit pending (UC-SUB-06, UC-SUB-10).

3.1 Proration with netting (UC-BIL-08)

At the switch, customer_billing produces a single invoice with two visible lines, and nets them so the customer pays the difference:

  1. the full price of the new subscription for its first period;
  2. a prorata credit note for the old subscription’s unused prepaid days (daily pro-rata on gross).

Each line carries its own recognition period; the filiation link is recorded.

Invariant — the billing anchor moves to the switch date. The new subscription opens with its own anniversary at the switch date; the anniversary intentionally drifts with each upgrade (UC-SUB-31, UC-BIL-08). The proration lives in the credit line; the new line is full price (this supersedes the billing draft’s “prorated new invoice” example).

4. Downgrade — end of paid period, no commercial refund

Design rule — a downgrade keeps current perks to the boundary; the cheaper subscription starts next cycle. The current subscription runs to cycle end with current perks kept; the cheaper subscription opens at the next boundary (close-then-open, filiation). There is no commercial proration / unused-time refund, because perks were retained until the boundary (UC-SUB-07).

Invariant — downgrades produce no credit note. Unlike an upgrade, no prorata credit line is generated (UC-BIL-08). Legally mandatory corrections still route through the refund machinery (UC-BIL-09).

5. Lateral move — same price, immediate

Design rule — a same-price move is immediate and treated like an upgrade; nothing is owed back (UC-SUB-08). It is a close-then-open with filiation; the netted invoice is zero.

6. Pending changes — only one, latest wins

Invariant — at most one pending change, and only a downgrade can sit pending. Since upgrades are immediate, the only schedulable change is a downgrade. A later upgrade cancels the scheduled downgrade (UC-SUB-10, UC-SUB-07).

7. Annual ↔ monthly and the proration regime

Design rule — “annual” is a conditional discount, not a no-exit lock. The bare payment-account framework contract is always resignable at will (PAD / CMF L.314-13); a true “12 months, no exit, no refund” lock on a payment account is a clause abusive and is not used. Commitment may attach only to the wrapper layer (savings/insurance), never the bare payment account (UC-SUB-09).

The annual price is realised by one of two regimesprepay-for-discount with pro-rata refund (N26 model) or discount clawback (Revolut model). The regime choice is open — see uncertainties.md (annual-proration regime). Statutory termination/pro-rata rights override via the framework-termination path (UC-SUB-30, UC-BIL-09).

8. Where the money is posted

Invariant — subscriptions emits the change; customer_billing posts the money. This crate performs the close-then-open, records filiation, and emits OfferChanged / UpgradeApplied / DowngradeScheduled. The proration arithmetic, the netted invoice, the credit line, and any pro-rata refund are computed and posted by customer_billing (UC-BIL-08, UC-BIL-09).

9. Related Documents

5. Bundles, Merge and Explosion

Bundles, Merge and Explosion

This document defines the bundle — one subscription over several modules at one price — its dismantle / explosion when a module inside it closes, and the reverse merge of unit subscriptions back into a cheaper bundle (UC-SUB-04/05/21).

1. Bundle = one subscription, many modules

Design rule — a bundle is a single subscription covering several modules at one price. S = (P, Valentine bundle, {current, shared}, active) — one subscription, one price, one snapshot (UC-SUB-04). The bundle subscription can cover several accounts/modules, but each of those accounts still belongs to that single subscription (UC-SUB-12).

Invariant — one active subscription per account holds inside a bundle too. A bundle does not create one subscription per module; the modules share the bundle’s single subscription and price (UC-SUB-04, UC-SUB-12).

2. Dismantle / explosion

Design rule — when a module inside a bundle closes, the bundle can’t stand, so the bundle subscription closes and surviving modules fall back to their unit offers at standard price. The fallback is a re-subscription / offer change (close-then-open, filiation). Net: the holder pays the sum of unit offers (more than the bundle); Green-Got does not keep providing a surviving module for free after the bundle predicate disappeared (UC-SUB-05, UC-OFF-07).

stateDiagram-v2
    [*] --> BundleActive: S = (P, bundle, {A, B})
    BundleActive --> ModuleClosed: a module (e.g. shared B) is closed
    ModuleClosed --> BundlePredicateBroken: bundle can't stand [UC-SUB-05]
    BundlePredicateBroken --> BundleExpired: close bundle subscription (close-then-open)
    BundleExpired --> FallbacksOpened: each surviving module → its unit offer at standard price
    FallbacksOpened --> [*]: holder pays sum of unit offers; filiation links to bundle

2.1 Customer-initiated dismantle — explicit consequence confirmation

Design rule — a customer-initiated dismantle requires explicit consequence confirmation. Before accepting the module closure, the UI/BO shows the surviving modules, their fallback offers, the next price, and the effective date. The customer can: confirm the new price, keep the bundle until period end, or close the surviving modules too (UC-SUB-05). BO-doable (UC-BO-02).

2.2 Forced dismantle

Design rule — a forced dismantle (legal/compliance/product impossibility) schedules the fallback and applies notice/rejection rules when price or framework terms materially increase. The system schedules the fallback and applies UC-REG-01 notice/rejection rules when the customer price or framework terms materially increase (UC-SUB-05).

Invariant — every surviving module resolves to an active fallback. Because explosion re-subscribes survivors to unit offers, those unit offers must exist and be active; conditional offers must declare a fallback verified continuously (Appendix A, UC-OFF-11).

3. Merge — the reverse of explosion

Design rule — when separate unit offers together qualify for a bundle or cheaper offer, move the holder onto the cheaper combination so they pay less — deliberately not during onboarding. It is done later, as a “good news” moment: the customer onboards for the new product as normal; once that onboarding is validated (by compliance or by the server), the system waits ~1 day, then automatically migrates at the next period boundary (close the unit subscriptions, open the bundle — close-then-open + filiation) and emails the customer that everything is now cheaper (UC-SUB-21).

stateDiagram-v2
    [*] --> Units: separate unit subscriptions S1, S2
    Units --> Qualified: S1+S2 together qualify for a cheaper bundle
    Qualified --> Validated: new onboarding validated (compliance/server)
    Validated --> Waiting: wait ~1 day
    Waiting --> BoundaryMigrate: at next period boundary
    BoundaryMigrate --> BundleActive: close units, open bundle (close-then-open + filiation)
    BundleActive --> [*]: email "everything is cheaper"
    Qualified --> SupportRoute: needs customer consent / not safe-auto
    SupportRoute --> [*]: alert customer support [UC-BO-08]

Design rule — boundary timing avoids mid-period refund mechanics. Migrating at the period boundary means no mid-period proration and no money owed back (UC-SUB-21, UC-BIL-09). Best bundle = the cheapest applicable one.

Invariant — a merge that would require customer consent is never auto-applied. If a merge needs consent (rare, since a price decrease normally needs none) or can’t be safe-automatic, the system alerts customer support instead of auto-applying (UC-SUB-21, UC-BO-08).

Invariant — merge respects collision/eligibility. The one-active-per-IBAN binding and offer eligibility still hold across the merge (UC-SUB-12, UC-SUB-21).

4. Individual → shared (support-only, related)

Design rule — transforming an individual account into a shared one is a support-only exception, performed as an offer change on the existing account. Customer support migrates the account to the shared-account subscription (an offer change to the shared offer) on the existing account — the account and money stay with the primary holder; no new account is provisioned, the account is not replaced — and invites the other person, who onboards separately as a participant (UC-SUB-13, UC-ONB-12). Participant capabilities are part of the shared-account module, not a separate module. Participant lifecycle is its own topic (UC-SUB-26).

5. Related Documents

6. Suspension, Closure and Death

Suspension, Closure and Death

This document defines the three terminal-or-restrictive paths: suspension (AML / fraud / security only), voluntary closure (request → async wind-down), and primary-holder death (manual termination) (UC-SUB-19/22/27). It also covers the at-will framework-contract termination with pro-rata refund (UC-SUB-30) and the negative-balance receivable (UC-SUB-24) that gates archival.

1. Suspension (AML / fraud / security only)

Design rule — suspension is a real subscription state used only for AML / fraud / security reasons, never for an unpaid fee. An unpaid fee produces a commercial arrears state instead (UC-SUB-14, UC-SUB-22). While suspended:

Invariant — suspension is never an account closure. It is lifted (or escalated to closure) by the outcome of the investigation (UC-SUB-22).

Design rule — authorisation is asymmetric. Anyone can suspend, with no delay (customer support who spots something weird, a compliance officer directly, or an automated rule); lifting is privileged — restricted to managers or compliance officers (UC-SUB-22).

Design rule — notification is mandatory with the reason by default, branched by a classified reason code. PSD2 art. 68 / CMF: the customer is informed (before or immediately after) and told why. No-tipping-off is a narrow exception, applying only when the block is linked to an AML suspicion (déclaration de soupçon to Tracfin, or an asset-freeze, LCB-FT / CMF L.561-x) — there we notify that the account is restricted without the reason. The branch is driven by a classified reason code, not operator discretion: fraud / security / risk-on-instrument → notify with reason; AML-suspicion-linked → notify without reason. Support cannot select the silent branch (UC-SUB-22).

Invariant — blocking funds movement and blocking account closure are lawful only in the AML-classified branch. An operational (art. 68) freeze blocks payment means but does not suspend the customer’s right to terminate the framework contract (UC-SUB-22, UC-SUB-23).

Design rule — proportionality: a support-initiated freeze has a max duration. Before that, it must be lifted or escalated to a named owner (fraud / compliance / AML officer) who confirms, reclassifies it as a formal AML measure, or releases it — it cannot sit indefinitely in “support spotted something” state (UC-SUB-22). The max-duration value and how billing behaves during suspension are open — see uncertainties.md (SUB-22).

stateDiagram-v2
    [*] --> Active
    Active --> Suspended: suspend (support / compliance / automated rule)
    Suspended --> SupportHold: support-initiated (provisional)
    SupportHold --> Escalated: escalate to named owner before max-duration
    SupportHold --> Active: released (privileged)
    Escalated --> AmlMeasure: reclassified as formal AML measure
    Escalated --> Active: released (privileged)
    AmlMeasure --> Active: lifted (privileged) [UC-SUB-22]
    AmlMeasure --> Closing: escalated to closure
    Suspended --> Active: lifted (managers / compliance only)
    note right of SupportHold
        Max duration TBD; cannot sit indefinitely.
        Billing-during-suspension: open. [SUB-22]
    end note

2. Voluntary closure — request → async wind-down

Design rule — a customer cannot truly close their account instantly; they request closure and the account enters an async wind-down. We collect payout/transfer instructions for remaining funds, show the commercial effective date (normally the end of the current paid period for a monthly commitment), and put the account into closing at that effective date (UC-SUB-19).

While closing:

Design rule — incoming credits after closure bounce; we do not redirect or manage them. Once closure is requested, incoming SEPA credits (salary, etc.) are returned to sender. We do not run forwarding/redirection; the customer tells their employer/payers and the receiving bank handles mobility (UC-SUB-19).

Invariant — archival is blocked until all ongoing matters settle. A non-empty account can still enter the closure flow (funds paid out during wind-down once legally and operationally available), but archival is blocked until all ongoing transactions, disputes, investigations (UC-SUB-22), legal seizures (UC-REG-05), and negative positions (UC-SUB-24) settle (UC-SUB-19).

stateDiagram-v2
    [*] --> Active
    Active --> ClosureRequested: customer request (collect payout instructions)
    ClosureRequested --> Closing: at commercial effective date (period boundary)
    Closing --> Closing: incoming SEPA credits bounce; pay out funds when available
    Closing --> Archived: all in-flight (txns/disputes/holds/seizures/negative) settled
    Archived --> [*]
    note right of Closing
        Subscription may already be Closed while the
        account wind-down continues — three lifecycles. [UC-SUB-33]
    end note

Design rule — returning the customer’s own balance is a payout, not a refund. Returning a customer’s own balance on closure is a payout of their own funds and does not use the refund machinery (UC-SUB-19, UC-BIL-09).

3. Commercial cancellation vs framework termination

Design rule — boundary cancellation is the default UX; at-will framework termination is the money-back path. Two distinct paths:

PathEffectRefund
Commercial cancellation (UC-SUB-23)Monthly: effective at period end, perks kept to boundary. Annual: resignable at will, unused prepaid months refunded pro-rata.No voluntary unused-time refund (monthly).
Framework-contract termination (UC-SUB-30, CMF L314-13)Effective on request, subject to a contractual notice capped at 30 days.Pro-rata prepaid-fee refund via the refund machinery (UC-BIL-09). Non-waivable — overrides the commercial “no refund” default.

Invariant — the legal at-will termination overrides the commercial no-refund default. The customer always retains at-will framework-contract termination with pro-rata prepaid-fee refund, which overrides the monthly “no unused-time refund” line when invoked (UC-SUB-23, UC-SUB-30). A pending cancellation can be revoked before the boundary; re-subscribing after the boundary on a still-open account is a normal subscribe / offer change (UC-SUB-23). The notice-period value is open — see uncertainties.md (SUB-30). Account wind-down follows §2.

4. Primary-holder death — manual termination

Design rule — on notification of the primary holder’s death, stop accruing subscription fees immediately, freeze the account, and handle the rest manually on the notary’s communication. We never keep billing a deceased customer’s estate (deliberately more conservative than the 2025 cap on frais bancaires de succession). Handling is manual: we act on the notary’s communication, which instructs the process (funds, payout, closure). This is a special case of manual termination, audited and reason-coded (UC-SUB-27, UC-BO-08, UC-REG-05).

Invariant — the ASV/life beneficiary-clause payout is legally distinct and not owned here. It is owned by the insurer/partner, not by customer_billing or subscriptions (UC-SUB-27, UC-REG-06).

5. Negative balance / offline card presentment

Design rule — a settled negative position is a real short-term receivable, distinct from the display-only projected balance. When a cleared transaction (offline/STIP EMV presentment, scheme fee) exceeds real funds, the available balance goes negative and Green-Got is owed. This is de facto short-term ancillary credit under PSD2 art. 18(4) — involuntary, short-term, ancillary, not funded from safeguarded client money — carrying a ≤12-month repayment ceiling and PI own-funds conditions (UC-SUB-24).

Invariant — the negative position is bounded and recovered, never offered as funds. A per-account cap restricts further authorisations beyond it; the position is recovered from the next incoming funds; if it persists beyond N days → dunning → restriction → write-off (UC-BIL-10); no revolving/rollover, never advertised as available funds (UC-SUB-24). The cap value and the recovery window N are open — see uncertainties.md (SUB-24). A negative position blocks archival until settled (§2).

6. Related Documents

7. Architecture

Architecture

This document fixes the intended DDD layering of the subscriptions crate, its segment-neutrality, the event boundary with customer_billing, and the closure Temporal workflow. It is design-only; the concrete module layout and build sequence live in plan.md.

1. Layering (DDD)

Per architecture/SKILL.md: a layered DDD structure, starting flat and promoting to full layer folders as complexity grows.

LayerContentsExamples
domain/aggregates, value objects, state machines, pure logic. Zero dependencies on other layers.Subscription, OfferSnapshot, SubscriptionModifier, SubscriptionStatus machine, cadence/anchor/boundary math, offer-change classification
use_cases/application use cases; orchestrate domain objects, hold no business rules themselves.open_subscription, change_offer, apply_upgrade, dismantle_bundle, suspend_subscription, request_closure
stores/persistence (records + the subscription store). Technical concerns only.subscription_store, models.rs
workflows/ + activities/the async closure wind-down (Temporal).closure_workflow, closure_activity
rules/eventbus subscriptions reacting to other crates.on_offer_retired, on_eligibility_lapsed, on_module_closed, on_account_closing

Design rule — pure domain, thin use cases, side-effect-free state machine. The SubscriptionStatus machine, cadence math, and offer-change classification are pure and unit-testable in isolation; use cases compose them and call the store/eventbus (UC-SUB-06, UC-SUB-33).

2. Segment-neutrality

Design rule — segment-neutral data + logic; no imports of segment systems. The crate references the generic banking / identity / investment / offers objects by id and owns none of them; it must not import retail-specific or B2B-specific systems (UC-SUB-12, Appendix A). Activation provisions the concrete instances (retail → person + payment account + card; B2B → organisation + business account) via the onboarding orchestrator; the generic crates only record the resulting links (UC-SUB-18).

Invariant — references in, no ownership out. The aggregate stores holder_id, account_ids, offer_id/offer_version, and (for savings) the investment-contract id; it never re-models the account, the identity, the offer catalogue, or the investment contract (UC-SUB-12, UC-SUB-15).

3. The event boundary with customer_billing

Design rule — subscriptions drives lifecycle state and emits events; customer_billing posts the money. Billing, recognition, and payment are three distinct concerns; this crate owns none of them. It emits the lifecycle events; customer_billing computes proration/netting/refunds and applies modifiers at the invoicing layer (UC-SUB-29, UC-BIL-08, UC-BIL-09, Appendix A).

sequenceDiagram
    autonumber
    participant SUB as subscriptions
    participant BILL as customer_billing
    participant ACC as accounting

    SUB->>SUB: change_offer → classify + close-then-open (filiation)
    SUB-->>BILL: OfferChanged / UpgradeApplied / DowngradeScheduled
    BILL->>BILL: read modifiers; compute proration + netted invoice (≥ 0) [UC-BIL-08]
    BILL->>BILL: post invoice + prorata credit line; collect from balance
    BILL-->>ACC: billing lines carry recognition period [Appendix A]
    Note over SUB,BILL: subscriptions never posts ledger lines; the amount lives in customer_billing

Invariant — modifiers are read each cycle and applied at the invoicing layer. The subscription declares the modifier set; customer_billing reads and applies it per cycle, so two holders of the same offer can be billed differently (UC-SUB-29).

4. The closure Temporal workflow

Design rule — voluntary closure is an async wind-down run as a Temporal workflow. The request_closure use case records the commercial effective date and starts the closure workflow; the workflow bounces incoming credits, pays out funds when available, and waits for all in-flight matters (transactions, disputes, holds, seizures, negative positions) to settle before the account can be archived (UC-SUB-19, UC-SUB-24). The subscription may already be Closed while the account wind-down continues — the three lifecycles do not collapse (UC-SUB-33).

5. Persistence shape

Design rule — one row per subscription generation, with a self-FK filiation link and a partial unique index enforcing one-active-per-IBAN. Each close-then-open writes a new subscription row carrying previous_subscription_id; a partial unique index over the covered account(s) for the active states enforces the one-active-subscription-per-IBAN invariant (UC-SUB-12, UC-SUB-32). At most one pending change (a downgrade) per subscription (UC-SUB-10). Closure/wind-down state lives on the account (owned by core_banking), not here (UC-SUB-33). Schema sketch in plan.md.

6. Tunables (parameters)

The launch-blocking-adjacent tunables are surfaced as parameters.rs and tracked in uncertainties.md: suspension max-duration & billing-during-suspension (SUB-22), negative-balance cap & recovery window (SUB-24), framework-termination notice period (SUB-30), and the annual-proration regime (SUB-09).

7. Related Documents

Uncertainties

Subscriptions — Active Register

This is an active, classified register of the open items in the subscriptions domain. The subscription model is settled and documented (the aggregate, the snapshot + filiation immutability contract, the three lifecycles, close-then-open offer changes, bundles, suspension, closure wind-down); this register tracks the remaining product-decision, legal, and tunable items until each is closed.

Status sections. Items move through: §1 Research pending (delegated to Claude to investigate, then close), §2 Resolved decisions (decided here; awaiting port into the named canonical doc), §3 Closing items (how an item is closed).

Where billing-amount items live. The amount of any change (proration, netting, refunds) is owned by customer_billing; this register holds only items genuinely owned by the subscriptions domain (lifecycle, tunables, regime selection). Where an item straddles both, the owning crate is named in the item.

Convention. “Launch-blocking = yes” means a real customer subscription/lifecycle path cannot run correctly until the item is closed. A resolved item that is launch-blocking stays launch-blocking until ported into its canonical doc.


1. Research pending (delegated to Claude)

These remain open until grounded findings are returned; the answer is documented in the named canonical doc and the item moved to §2.

SUB-22a — Support-freeze max duration

Question: The maximum duration a support-initiated (provisional, art. 68) freeze may sit before it must be lifted or escalated to a named owner (fraud / compliance / AML officer). The proportionality rule is documented; only the concrete duration value is open (UC-SUB-22).

SUB-22b — Billing behaviour during suspension

Question: How does billing behave while a subscription is suspended (AML/fraud)? Does the recurring charge keep accruing, pause, or waive for the suspension window — and does the answer differ between the art. 68 operational branch and the AML-classified branch? Marked OPEN in the use case (UC-SUB-22).

SUB-24 — Negative-balance cap and recovery window N

Question: The per-account cap beyond which no further authorisations are permitted, and the recovery window N days after which a persistent negative position escalates to dunning → restriction → write-off. Both must sit inside the PI ancillary-credit envelope (art. 18(4), ≤12-month ceiling, own-funds only). The mechanism is documented; the two numeric values are open (UC-SUB-24).

SUB-30 — Framework-termination notice period

Question: The concrete contractual notice period for at-will framework-contract termination, capped at 30 days (CMF L314-13). The cap and the pro-rata-refund mechanic are documented; the chosen value within the cap is open (UC-SUB-30).

SUB-09 — Annual proration regime (prepay-refund vs discount-clawback)

Question: Which of the two documented regimes realises the annual price — prepay-for-discount with pro-rata refund (N26 model) or discount clawback (re-rate consumed months at standard monthly on early exit; Revolut model)? Both are lawful (early exit forfeits the discount, never access/money); the choice drives the refund machinery and the customer_billing contract (UC-SUB-09, UC-BIL-08).


2. Resolved decisions (awaiting port into canonical docs)

None yet. Decisions taken in this round are already reflected in the canonical docs:


3. Closing items

An item is closed by recording the confirmed value/decision in the owning subscriptions doc (and, for tunables, by pinning the value in parameters.rs). SUB-22a, SUB-22b, SUB-24, SUB-30, and SUB-09 are launch-blocking for their respective paths (suspension, card spend, self-service termination, annual cadence).

4. Related Documents

Compliance

French banking

ACPR Payment Institution authorisation dossier

ACPR Payment Institution Authorisation Dossier

Source: /Users/enrico/Downloads/GREEN-GOT_Dossier_Agrement_EP_v1_011223_Bpart.docx.

This English Markdown conversion preserves the dossier’s substantive representations, commitments, named entities, regulatory references, dates, figures, annex references, hyperlinks, tables, and operational/security process descriptions. Decorative images and low-value screenshots are omitted. Mobile application screenshots are represented as journey steps, and process, organization, architecture, security, financial and calendar visuals are represented as text-first descriptions or tables.

Original Source Title

License Application Dossier - Payment Institution (Établissement de Paiement, EP)

I, the undersigned Andréa GANOVELLI, in my capacity as Chief Executive Officer of DOMINO SAS, certify the accuracy of the information below and undertake to inform the Autorité de contrôle prudentiel et de résolution of any changes made to any item included in this form.

Signed in Paris, on 10/01/2024

Signature

Introduction

Person responsible for the dossier:

FieldInformation
TitleMr
Last nameGANOVELLI
First nameAndréa
Title/functionChief Executive Officer of Green-Got
Telephone no.06 01 19 25 62
Emailandrea@green-got.com

Person to contact for any question about the dossier:

FieldInformation
TitleMs
Last nameLARRE
First nameLola
Title/functionAssociate Director of the firm B-Part
Telephone no.06 73 70 55 83
Emaill.larre@bpart-consulting.com

Signature of the person, legal representative of the company:

FieldInformation
Last nameGANOVELLI
First nameAndréa
FunctionChief Executive Officer
Date10/01/2024

Glossary

ACPR: means the Autorité de Contrôle Prudentiel et de Résolution.

Agent: means a natural or legal person acting on behalf of a payment service provider for the provision of payment services, in accordance with Article L.523-1 of the CMF.

Mobile Application: means the Green-Got mobile application.

Arkéa: here means a set of three distinct legal entities:

The institution or institutions that will hold the various accounts (safeguarding/ring-fenced account (compte de cantonnement), settlement account, and operating account) have not yet been finalized and will depend on the specifications of the final product and the final contracts. The Sponsorship letter is therefore signed by both ABS and CMA. Whatever the exact allocation of responsibilities among the three entities, there will be a single point of contact for DOMINO SAS. In the rest of the document, DOMINO SAS therefore considers these three entities together as one Partner named Arkéa.

Strong authentication (Authentification forte): means the procedure allowing DOMINO SAS, as Payment Service Provider, to verify the identity of a payment service user or the validity of the use of a specific payment instrument, including the use of the user’s personalized security credentials. In accordance with Article L.133-4 of the CMF, this authentication is based on the use of two or more elements belonging to the categories “knowledge” (something only the user knows), “possession” (something only the user possesses), and “inherence” (something the user is), which are independent in the sense that compromise of one does not call into question the reliability of the others, and which are designed to protect the confidentiality of the authentication data.

Card (Carte): means the Green-Got payment card, a Payment Instrument enabling Payment Transactions to be carried out, issued by DOMINO SAS.

Client: means the natural or legal person who has subscribed to DOMINO SAS’s payment service offering, under the Green-Got brand, and whose business relationship onboarding (entrée en relation) has been validated by the Compliance function.

CMF: means the French Monetary and Financial Code (Code monétaire et financier).

Payment Account (Compte de paiement): means, in accordance with Article L.314-1 of the CMF, an account held in the name of one or more payment service users, used for the execution of a Payment Transaction.

PSD2 (DSP2): means Directive (EU) 2015/2366 of the European Parliament and of the Council of 25 November 2015 on payment services in the internal market.

Client Area (Espace client): means a Client’s personal area accessible from the Mobile Application or the Website, allowing the Client in particular to view the Payment Account balance or execute a Payment Transaction.

GDA: means the asset-freezing mechanism (Gel des avoirs) provided for in Article L.562-4 of the CMF.

Payment Instrument (Instrument de paiement): means, in accordance with Article L.133-4 of the CMF, any personalized device and/or set of procedures agreed between the payment service user and DOMINO SAS, in its capacity as Payment Service Provider, and used to initiate a Payment Order.

AML/CFT (LCB-FT): means the fight against money laundering and terrorist financing (Lutte Contre le Blanchiment de capitaux et le Financement du Terrorisme).

Payment Transaction (Opération de paiement): means, in accordance with Article L.133-3 of the CMF, an action initiated by the payer or on the payer’s behalf, or by the beneficiary, consisting of depositing, transferring, or withdrawing funds, irrespective of any underlying obligation between the payer and the beneficiary.

Payment Order (Ordre de paiement): means an instruction from a payer or beneficiary to its payment service provider requesting the execution of a Payment Transaction.

Direct Debit (Prélèvement): means, in accordance with Article D.314-2 of the CMF, a payment service intended to debit a payer’s payment account, where a payment transaction is initiated by the beneficiary on the basis of consent given by the payer to the beneficiary, to the beneficiary’s payment service provider, or to the payer’s own payment service provider.

Provider (Prestataire): means the companies with which DOMINO SAS has contracted for the provision of a service enabling the provision of its offerings.

Prospect: means a natural or legal person interested in entering into an offer with DOMINO SAS and whose onboarding process has not yet been validated by the Compliance function.

PSEE: means an Essential Outsourced Service Provider (Prestataire de Services Essentiels Externalisés) in accordance with the Order of 3 November 2014 on the internal control of companies in the banking, payment services, and investment services sectors subject to ACPR supervision.

Payment Services (Services de Paiement): means the payment services offered by DOMINO SAS to its Clients, in accordance with Article L.314-1 of the CMF, for which DOMINO SAS is making this license application.

Website: means the DOMINO SAS website, operating the Green-Got brand: https://green-got.com.

Transfer (Virement): means, in accordance with Article D.314-2 of the CMF, a payment service provided by a payment service provider that holds a payer’s Payment Account and consists of crediting, on the basis of the payer’s instruction, the Payment Account of a beneficiary by one or more payment transactions made from the payer’s payment account.

The Company

General Information

FieldInformation
Corporate nameDOMINO
Trade name if differentGeen-Got
Siren883 981 763
Legal formSociété par actions simplifiée
Registered office address20 bis rue Louis Philippe
Postal code92200
CityNeuilly-sur-Seine
CountryFrance
Telephone no.06 01 19 25 62
Emailcontact@green-got.com
Websitehttps://green-got.com

Information Relating to Share Capital

FieldInformation
Amount of share capital in EUREUR 24,809.70
Amount of capital to be paid up in EUREUR 13.5M fundraising scheduled for 2024 (year N-1 of DOMINO SAS’s BP)
Effective dateAfter obtaining the license, subject to conditions precedent

Group Structure

Does the company belong to a group? No.

The K-bis (1.1 K-bis of DOMINO SAS) and the bylaws (1.2 Bylaws of DOMINO SAS) of DOMINO SAS are provided in the annexes.

Shareholding

The shareholding structure of DOMINO SAS is as follows:

ShareholderShare of capital / voting rights
Maud CAILLAUX19.84%
Andréa GANOVELLI19.84%
Fabien HUET16.82%
Pale Blue Dot9.85%
Crowdcube Nominees Limited5.42%
David MOISON4.62%
Romain DURAND2.74%
Area Climate and Frontier Fund2.72%
Various natural / legal persons18.15%

Qualified shareholders, direct or indirect, natural or legal persons, holding more than 10% of the capital or voting rights are as follows:

Direct Shareholders

Name / corporate nameShare of capital in %Share of voting rights in %
Shareholder 1Maud CAILLAUX19.8419.84
Shareholder 2Andréa GANOVELLI19.8419.84
Shareholder 3Fabien HUET16.8216.82

The following are provided in the annexes:

Indirect Shareholders

To date, no indirect shareholder holds more than 10% of the capital or voting rights of DOMINO SAS.

However, it should be emphasized that Pale Blue Dot1, a Swedish investment fund, holds 9.85% of the capital and voting rights of DOMINO SAS.

Pale Blue Dot Investment A.B is a private company under Swedish law, registered in Malmö. Its main activity consists of investing at the pre-seed and seed stages in young companies specializing in impact sectors, biodiversity protection, and greenhouse gas emissions reduction, particularly in agriculture, logistics, or ESG data provision, in Europe and the United States.

The company was created in March 2020 by its founders:

To date, Pale Blue Dot manages a portfolio of EUR 180M and has not yet invested in regulated entities.

Shareholders’ Agreement

(or any concerted action within the meaning of Article L.233-10 of the French Commercial Code)

Have the shareholders signed a shareholders’ agreement? Yes.

The shareholders’ agreement signed between Ms Maud CAILLAUX, Mr Andréa GANOVELLI, Mr Fabien HUET and Pale Blue Dot Investments AB, Aera Climate and Frontier Fund, Mr David Moison, Mr Romain Durand, and TED is provided in the annexes (1.3 Shareholders’ agreement).

Effective Managers

The Effective Managers (Dirigeants Effectifs) of the future Payment Institution of DOMINO SAS are:

The appointment form as Effective Manager of Ms CAILLAUX (3.1.1 Appointment form as effective manager of Ms CAILLAUX), her corporate officer contract (3.1.2 Corporate officer contract of Ms CAILLAUX), and a copy of the document appointing Ms CAILLAUX as effective manager (3.1.3 Extract from the minutes appointing Ms CAILLAUX as effective manager) are provided in the annexes.

In addition, the CV of Ms CAILLAUX (2.1.2), her identity document (2.1.3), her non-conviction declaration (2.1.4), and her criminal record extract (2.1.5) are also provided in the annexes.

The appointment form as Effective Manager of Mr GANOVELLI (3.2.1 Appointment form as effective manager of Mr GANOVELLI), his corporate officer contract (3.2.2 Corporate officer contract of Mr GANOVELLI), and a copy of the document appointing Mr GANOVELLI as effective manager (3.2.3 Extract from the minutes appointing Mr GANOVELLI as effective manager) are provided in the annexes.

In addition, the CV of Mr GANOVELLI (2.2.2), his identity document (2.2.3), his non-conviction declaration (2.2.4), and his criminal record extract (2.2.5) are also provided in the annexes.

Supervisory Body

DOMINO SAS will create a supervisory body in the form of a Supervisory Board (Conseil de Surveillance), whose operation is specified in the bylaws. This body will be composed of five individual members.

To date, the composition of the Supervisory Board is as follows:

The two missing members are being appointed and will be independent members. Their identities, as well as the information below, will be sent after their appointment.

Similarly, the identity of the Chair of the Supervisory Board will be communicated after the two missing members are appointed.

Andréa GANOVELLI, Member of the Supervisory Board

FieldInformation
TitleMr
Usual nameGANOVELLI
Family nameGANOVELLI
First nameAndréa
Other first namesYannick, Adrien
Date of birth27/06/1992
Country of birthFrance
Municipality of birthNice
Postal code of municipality of birth06100
NationalityFrench
Other nationalityN/A
Address33 rue des Deux ponts
Postal code75004
CityParis
CountryFrance
FunctionMember of the Supervisory Board
Start dateUpon obtaining the license

Kaïss BOUSRY, Member of the Supervisory Board

FieldInformation
TitleMr
Usual nameBOUSRY
Family nameBOUSRY
First nameKaïss
Other first namesN/A
Date of birth22/08/1992
Country of birthCOMOROS
Municipality of birthMORONI
Postal code of municipality of birth99
NationalityFrench
Other nationalityN/A
Address8 RUE JEAN DE LA FONTAINE
Postal code44000
CityNANTES
CountryFRANCE
FunctionMember of the Supervisory Board
Start dateUpon obtaining the license

Pierre-Manuel SCROZYNSKI, Member of the Supervisory Board

FieldInformation
TitleMr
Usual nameSCROZYNSKI
Family nameSCROZYNSKI
First namePierre-Manuel
Other first namesN/A
Date of birth31/07/1962
Country of birthFrance
Municipality of birthBriey
Postal code of municipality of birth54150
NationalityFrench
Other nationalityN/A
Address27C rue des fours à Chaux
Postal code63118
CityCebezat
CountryFrance
FunctionMember of the Supervisory Board
Start dateUpon obtaining the license

Committees

To date, in accordance with the Shareholders’ Agreement, DOMINO SAS has established a Strategic Committee composed of the following members:

This Committee meets whenever a strategic decision must be adopted, and at least three times per year. The strategic decisions in question include:

As part of this Payment Institution license application, DOMINO SAS will establish the following non-statutory committees:

Subjection to a Competent Authority in the Financial Services Sector

DOMINO SAS has not been and is not subject to a competent authority in the financial sector.

Membership in a Professional Association

DOMINO SAS plans to join AFEPAME (Association Française des Établissements de Paiement et de Monnaie Électronique). This membership will become effective when the License is granted.

Program of Activities

Planned effective start date of the activity: Q4 2024.

Description of the Activity

Genesis of the Project

DOMINO SAS, operating under the trade name Green-Got, is a French impact Fintech serving the ecological transition. It was founded in 2020 by Ms Maud CAILLAUX, Mr Andréa GANOVELLI, and Mr Fabien HUET.

“Green-Got” was born from the observation of the urgent need to increase financing for the ecological transition and protection of the planet. In 2023, climate change remained, despite the deteriorated economic and geopolitical context, the 3rd greatest concern of the French2.

Many people act in their daily lives and consumption toward more responsible alternatives, but without any genuinely satisfactory solution to financially support the transition, while the financial savings of French households reached nearly EUR 6,000 billion in 2023.

Since its launch in June 2022, DOMINO SAS has offered its Clients so-called “ordinary” Payment Services, namely an account/card offering, as an Agent of Payment Service Provider PPS EU SA, a subsidiary of the Edenred Group and Electronic Money Institution licensed by the National Bank of Belgium, operating under the trade name Edenred Payment Solutions.

The Payment Services currently offered by DOMINO SAS are as follows:

Via their Client Area, accessible from the Green-Got Mobile Application or the Website https://green-got.com, Clients can:

In addition, DOMINO SAS also offers the following features:

Since 01 December 2023, DOMINO SAS has also been registered with ORIAS as a Financial Investment Advisor (Conseiller en Investissement Financier, CIF) in order to distribute life insurance contracts. This product is provided in partnership with Generali.

Through interchange fees and rounding, DOMINO SAS finances ecological projects, including ocean protection, reforestation, and the fight against deforestation. DOMINO SAS thereby enables its Clients, with each payment, to participate in financing the ecological transition.

This positioning enabled DOMINO SAS to convince, as of 21/12/2023:

DOMINO SAS was able to finance its development by combining the following financial levers:

These funds enabled DOMINO SAS to continue improving its “account/card” offering, a number of non-regulated services complementary to that offering, such as calculating the CO2 impact of expenses made, to carry out marketing expenditure to increase its awareness and acquire new clients, and to develop an Application, a Website, and IT tools complementary to those offered by its Partner PPS EU SA, including an internal back office accessible to teams for AML management, internal communication, and customer support.

Building on the confidence of its shareholders and Clients, DOMINO SAS wishes to expand its activity along several complementary lines:

To achieve these development lines, DOMINO SAS must be able to internalize a large part of the key functions currently performed by PPS EU SA, and therefore gradually free itself from them, which led it to file this Payment Institution license application.

Ambitions and Development Targets of DOMINO SAS

Relying on a relatively large client portfolio, DOMINO SAS wishes to leverage its experience as an Agent of a Payment Service Provider in order to become one of the major players in “green Fintech”, working in favor of the ecological transition in France and Europe.

This ambition relies on obtaining the Payment Institution license that is the subject of this application, but also on acquiring a new client segment and evolving its product and service offering.

Regulatory Strategy (Payment Services)

For the provision of Payment Services to its clientele, DOMINO SAS has defined the following regulatory strategy:

Broadening the Clientele

To date, DOMINO SAS targets a clientele consisting exclusively of Natural Persons, individuals and entrepreneurs, resident in France and Belgium.

As of 06/12/2023, the average age of DOMINO SAS’s individual clientele was 32. 54% were male and 46% female.

They are mainly residents of large cities with more than 100,000 inhabitants, with 25% of the clientele based in Paris or the Paris region. They all share a strong interest in environmental topics.

To acquire new clients and consolidate the relationship with the existing clientele, DOMINO SAS focuses on:

As part of its Payment Institution license, DOMINO SAS wishes to offer its range of products and services to:

Expansion of the Offering

As part of its roadmap, DOMINO SAS wishes to complete the current Payment Services offering with new features and also offer new products and services:

DOMINO SAS has set up waiting lists open to anyone, already a client or not, which enables it to:

Thus, in parallel with this Payment Institution license application, DOMINO SAS will launch the registration process as IOBSP of CFCAL in order to offer bank savings solutions, such as passbooks.

Offer Marketing Methods

As a Payment Institution, DOMINO SAS will market the Green-Got offer to Natural Persons and Legal Persons online and directly, without using a direct distribution partner network.

The switch from Agent status to Payment Institution status will not change DOMINO SAS’s Marketing and communication strategy, which will continue to rely on strengthening its brand image: a strong brand, committed to an important mission and transparent about its actions, enabling client acquisition driven by word of mouth.

DOMINO SAS will continue to use the acquisition channels described in paragraph 5.1.2, adding other channels, in particular:

The Green-Got offer will be structured around a pricing grid and Standard and Premium packages enabling a more precise response to the needs of the different categories of clientele targeted by DOMINO SAS.

Transition from Agent Status to Payment Institution Status

DOMINO SAS teams have acquired solid experience and a deep culture in compliance and regulation as an Agent.

Indeed, PPS EU delegated the following operational and control tasks to DOMINO SAS as part of its Agent activity:

In this context, DOMINO SAS:

These numerous delegations, supervised by internal and external controls, in particular from PPS EU, constitute a very positive factor in preparing the transition from Agent status to Payment Institution status.

From an operational and technical standpoint, the Agent / Payment Institution transition will take place through a gradual migration, presented in detail in section 11 “Project Implementation Timeline”.

Contract Mapping

The contract map containing the projected partnerships for the provision of DOMINO SAS’s offering is as follows:

Relationship shown in the contract mapContracts / counterparties
Client contracts under the Agent setupGreen-Got / PPS EU / Belgian end user relationships, including the Prepaid Alternative Banking Services Agreement, the Green-Got account and prepaid debit card agreement, and Green-Got general terms.
Client contracts under the Payment Institution setupGreen-Got / French end user relationships, including Green-Got general terms and the Payment Institution payment account framework contract.
Provider contractsGreen-Got contracts with the SEPA lead bank / safeguarding bank, Mastercard, PSEEs, and other providers.
  1. Prepaid Alternative Agreement: agreement entered into between DOMINO SAS and PPS EU SA on 14/10/2021 for the provision of Payment Services to DOMINO SAS’s clientele as Agent of a Payment Service Provider, which will continue to apply after obtaining the Payment Institution license for the provision of Payment Services to Belgian clientele (4.1 Prepaid Alternative Agreement entered into with PPS).

  2. Green-Got General Terms and Conditions: agreement currently entered into between DOMINO SAS and its Clients as part of its activity as Agent of PPS EU SA, which will continue to apply after obtaining the Payment Institution license for the provision of Payment Services to Belgian clientele (4.2 Green-Got General Terms and Conditions as Agent).

  3. Green-Got account and prepaid debit card agreement: tripartite agreement currently entered into between DOMINO SAS, PPS EU SA, and its Clients as part of its activity as Agent of PPS EU SA, serving as a framework payment services agreement within the meaning of the Regulations. This agreement will continue to apply after obtaining the Payment Institution license for the provision of Payment Services to Belgian clientele (4.3 Green-Got account and prepaid debit card agreement).

  4. Green-Got General Terms and Conditions: agreement to be entered into between DOMINO SAS and its Clients, individuals and professionals, as part of its Payment Institution activity (4.4 Green-Got General Terms and Conditions as Payment Institution).

  5. Agreement for opening and maintaining the safeguarding account (compte de cantonnement): DOMINO SAS wishes to contract with Arkéa for the opening and maintenance of its safeguarding account. Discussions have already been initiated between the two parties and DOMINO SAS’s KYC is being carried out by Arkéa’s teams. Pending provision of that agreement, an accompanying letter to the license application drafted by Arkéa is provided in the annexes (4.5 Arkéa letter supporting Green-Got’s license application).

  6. Memorandum of understanding: DOMINO SAS wishes to contract with Mastercard in order to obtain a Mastercard card issuing license (Principal Member) and processing. Pending contracting between the two entities, Mastercard has drafted a Memorandum of understanding specifying, in particular, Mastercard’s willingness to cooperate with DOMINO SAS on a number of subjects (4.6 Memorandum of understanding drafted by Mastercard).

  7. Agreement relating to SEPA lead bank services: DOMINO SAS wishes to contract with Arkéa for SEPA Lead Bank services. Pending provision of that agreement, an accompanying letter to the license application drafted by Arkéa is provided in the annexes (4.5 Arkéa letter supporting Green-Got’s license application).

  8. HAWK:AI SaaS Agreement: agreement entered into between DOMINO SAS and Hawk AI GmbH since 31 July 2021 for screening and transaction monitoring as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.7 Hawk:AI SaaS Agreement).

  9. Remote identity verification service agreement: agreement entered into between DOMINO SAS and NJF Vision SAS (Ubble) for online identity verification as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.8 Remote identity verification service agreement).

  10. General terms of sale, provision, and use of Efficiale lists and services: agreement entered into between DOMINO SAS and Efficiale SAS for screening Politically Exposed Persons and sanctioned persons as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.9 General terms of sale, provision, and use of Efficiale lists and services).

  11. AWS Service Terms: agreement entered into between DOMINO SAS and AWS for cloud hosting as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.10 AWS Service Terms).

  12. Card Manufacturing Services Agreement: DOMINO SAS wishes to contract with Exceet Card AG for Card Manufacturing Services, meaning card production and distribution. Pending contracting between the two entities, Exceet has drafted a document formalizing its partnership with DOMINO SAS (4.11 Partnership Agreement for Integration and Production phases drafted by Exceet).

  13. Agreement with ECAI: agreement entered into between DOMINO SAS and ECAI for bookkeeping, preparation of VAT returns and periodic tax returns, and preparation of annual accounts, as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.12 ECAI engagement letter).

  14. Agreement with Stripe: agreement entered into between DOMINO SAS and Stripe for acquiring payments made by bank card, as part of the activity as Agent of a Payment Service Provider. DOMINO SAS wishes to continue the relationship with this Provider (4.13 Stripe services agreement).

  15. Agreement with Invoke: DOMINO SAS wishes to contract with Invoke for regulatory reporting (4.14 Invoke SaaS service terms of use).

List of Payment Services to Be Provided by DOMINO SAS

(Article L.314-1, II of the French Monetary and Financial Code)

The envisaged types of payment service are as follows:

Payment serviceSelectedNotes
Services enabling cash to be placed on a payment account and operations required for operating a payment account
Services enabling cash withdrawals from a payment account and operations required for operating a payment account
Execution of the following payment transactions associated with a payment account
a) Direct debits, including one-off direct debits
b) Payment transactions made with a payment card or similar device
c) Transfers, including standing orders
Execution of the following payment transactions associated with a credit opening
a) Direct debits, including one-off direct debits
b) Payment transactions made with a payment card or similar device
c) Transfers, including standing orders
Including the granting of credit compliant with the conditions set out in II of Article L.522-2 of the French Monetary and Financial Code: yes ☐ - no ☐
Issuance of payment instruments
Acquisition of payment transactions
Including the granting of credit compliant with the conditions set out in II of Article L.522-2 of the French Monetary and Financial Code: yes ☐ - no ☒
Money remittance services
Payment initiation services
Account information services

It should be emphasized that DOMINO SAS, as a Payment Institution, will offer the same payment services as those it currently offers as Agent of PPS EU SA.

Description of the Payment Services Proposed by DOMINO SAS

Proposed Distribution Channels

DOMINO SAS wishes to make the Green-Got offer available to its Clients through:

Onboarding Process

A prospect wishing to subscribe to the Green-Got offer proposed by DOMINO SAS and thereby become a Client may do so only through the Mobile Application.

It is not planned, in the short term, to allow clients to subscribe to Green-Got offers through the Website. The initial onboarding will take place through the Mobile Application only, and the Website will include a link redirecting to download the Mobile Application.

The screenshots corresponding to the onboarding process for a Natural Person are represented below as journey steps. The onboarding process for a Legal Person is under construction, but it will be similar, with a few differences, to the one described below.

DOMINO SAS has designed a continuous process that currently allows the onboarding of a Natural Person, the opening of that person’s Payment Account, the initial funding of that Account, and the ordering of the physical payment Card in the same sequence of actions.

This process ensures a smooth and secure user experience while complying with legal and regulatory obligations, particularly in relation to data protection, anti-fraud, and AML/CFT.

  1. Welcome screen:
    • The User starts by choosing between registering for a new account or logging into an existing account.
    • The User then chooses the product type: individual account (retail) or micro-enterprise (professional). These choices will be completed as new offers are launched, including the offer for Legal Persons (company) and Premium offers.
  2. Registration:
    • The User fills in personal information, such as last name, first name, date of birth, etc.
  3. Verification of the User’s email address:
    • The User’s email address is verified via a code sent by email, confirming the authenticity of that address.
  4. Creation of the Personal Code:
    • The User chooses a Personal Code to access the Application, with an option for biometric security.
  5. Choice of Card:
    • The User selects the type of Card to which the User wishes to subscribe.
  6. Delivery information:
    • The User enters the delivery address for the previously selected Card. Delivery times depend on manual validation of the account by DOMINO SAS, which is performed only on business days. However, DOMINO SAS has no direct control over Card manufacturing and delivery times and therefore cannot commit on this point.
  7. Address validation:
    • The User confirms and validates the delivery address.
  8. Green-Got offer:
    • The price and benefits associated with the Green-Got offer are presented to the User.
  9. Preamble: Identity Validation:
    • The User is introduced to the identity verification step.
  10. Checklist of documents required for identity verification:
  1. Verification and validation of the User’s identity via Ubble:
  1. Top-up:
  1. GDPR checks:
  1. Additional information on the User:
  1. Communication choices:
  1. Finalization and congratulations screen:
  1. Validation of the User by the DOMINO SAS Compliance Function:
  1. Transmission of data to FICOBA:
  1. Transmission of data to the Card manufacturer / personalizer (Exceet), see the section “Issuance of a payment card” for more details:
  1. Address validation:

Issuance of a Payment Card

The payment card issuance process occurs only after completion of the KYC (Know Your Customer) process and data collection.

Process start.

Client origin:

Clients may initiate the Card issuance process when:

Because this is a Client-originated process, it requires automated compliance checks. If a flag is raised by DOMINO SAS’s internal systems, for example following card reorders that are too frequent or a change of address, the process becomes manual.

The cost for the Client to reissue a Card is EUR 10, except for exceptional commercial gestures.

The flow in the application is as follows:

  1. Card screen: the User clicks “Oppose”.
  2. Card reorder screen: the User can lock the Card if the User is not certain about cancelling the Card, or order a new Card, which will deactivate the current Card.

Internal origin:

When Clients complete their onboarding, the DOMINO SAS Compliance Function validates them and thereby starts the issuance process for the Client’s first physical Card.

In addition, the Customer Service Function may initiate the reissuance process on behalf of the Client.

Automatic origin:

When the Card is about to expire, an automatic process is in place to initiate the reissuance process.

Card creation.

Creation and tokenization inside the PCI DSS-compliant module:

At this step, DOMINO SAS creates the Card internally. The Card object will contain:

Except for the Card token and partial PAN, these data will remain in a module separate from the rest of the system and will be extracted only for specific needs. This is DOMINO SAS’s tokenization process.

Activation of virtual Cards:

For virtual Cards, activation is immediate and they can be used immediately without any additional step.

Physical Cards: integration module with the card bureau:

For physical cards, DOMINO SAS’s integration module with the Card bureau (Exceet) will prepare Card data in batches and transmit them to the bureau through encrypted channels.

Card bureau process.

Card manufacturing:

Quality controls are performed by Exceet. Card manufacturing begins and is finalized on the 1st business day following receipt by Exceet of the Card personalization information.

Card packaging:

The Cards are packaged, prepared for delivery, and sent. Card packaging is carried out manually directly after manufacturing.

Shipping:

Cards are sent to Deutsche Post and delivered to the address entered by the Client twice per week, on Wednesday and Friday; Deutsche Post is responsible for shipping them directly to the Client.

Under the current “Agent” setup, cards are delivered no later than D+5 business days after validation of the order. This is also the objective for the “Payment Institution” setup, but this is being validated with Exceet.

Activation by the Client:

Upon receipt, Clients activate the Card online by providing the last 4 digits of the Card. The Card can then be used.

Activation by Mastercard:

DOMINO SAS completes the process by transmitting the relevant data, such as tokenized Card details, to the card network (Mastercard) for final integration into its payment system.

Funding a Payment Account

Funding by Card

This paragraph describes the process for funding the Client’s Payment Account by Card. This process involves Stripe (collection of Card data, transaction processing and settlement), Arkéa (safeguarding of funds), Hawk:AI (post-transaction security analysis), and DOMINO SAS (maintenance of Client Payment Account balances and accurate records of each transaction in its ledger).

  1. The Client initiates a top-up via the Application:
    • The Client logs into the Mobile Application and selects the account top-up option.
    • The Client enters the desired top-up amount and the details of the Client’s personal bank card, which are processed through the Stripe payment gateway integrated into the Mobile Application.
  2. Processing and fraud controls by Stripe:
    • As acquirer and processor, Stripe collects the Client’s personal bank card information and processes the Card transaction, including authorization, authentication, and ACS if needed.
    • Stripe performs the necessary fraud and compliance controls as part of providing its service.
  3. Settlement of payments in batches by Stripe:
    • Stripe aggregates all top-up transactions for DOMINO SAS and processes them in batches.
    • At the end of the day or another predetermined settlement period, Stripe sends the total amount of all top-ups by bank transfer to DOMINO SAS’s settlement account at Arkéa.
  4. Notification from Arkéa to DOMINO SAS:
    • Arkéa informs DOMINO SAS of the batch transfer received from Stripe.
    • Arkéa’s role is primarily to receive the funds, and the bank does not participate actively in processing top-ups.
  5. Transfer to a safeguarding account:
    • DOMINO SAS then instructs Arkéa to transfer the funds received from the settlement account to the safeguarding account, also opened and held by Arkéa.
    • This step is crucial for protecting client funds.
  6. Updating client accounts and ledger by DOMINO SAS:
    • DOMINO SAS updates the balance of each Client’s individual Payment Account to reflect the new top-up.
    • Transactions are recorded in DOMINO SAS’s transaction ledger, ensuring precise tracking of all movements of client funds.
  7. Post-transaction monitoring by Hawk:AI:
    • After finalization of the transaction, Hawk:AI analyzes the transactions to detect any potential fraudulent activity. This adds an additional security layer to the process.
  8. Post-transaction settlement of Stripe fees:
    • After the transaction, Stripe fees are added to the monthly invoice paid by DOMINO SAS.
Funding by Transfer

This paragraph describes the process for funding the Client’s Payment Account by a transfer initiated from the Client’s Payment Service Provider, which processes and sends the funds to Arkéa. Upon receipt, Arkéa informs DOMINO SAS, which updates the balance of the Client’s Account and its ledgers.

Processing is performed upon receipt by DOMINO SAS: within D+1 at most for a standard SCT and immediately for an SCT Inst.

  1. The Client initiates a transfer from the Client’s Payment Service Provider (PSP):
    • The Client accesses the PSP’s online platform or goes to a physical branch. The Client configures a transfer by entering the details of the Payment Account opened with DOMINO SAS.
  2. Processing by the External Bank:
    • The PSP processes the transfer request, checking the Client’s account to ensure sufficient funds and checking the transaction details. Security checks may be performed to authenticate the transaction. The PSP sends the funds via SEPA, a secure interbank payment system such as SWIFT, to the dedicated settlement account opened by DOMINO SAS with Arkéa.
  3. Arkéa processing:
    • Arkéa receives the transfer on DOMINO SAS’s settlement account.
    • Arkéa informs DOMINO SAS of receipt of the funds.
  4. DOMINO SAS processing:
    • DOMINO SAS verifies the transfer details and allocates the funds to the relevant Client’s Payment Account.
    • The balance of the Payment Account opened by the Client with DOMINO SAS is updated to reflect the funding by transfer.
    • The transaction is added to the general ledger.
    • The Client receives confirmation of successful funding by transfer via the Application or an email.
    • DOMINO SAS then instructs Arkéa to transfer the funds received from the settlement account to the safeguarding account, also opened and held by Arkéa.
  5. Safeguarding:
    • Arkéa transfers the funds from the settlement account to the safeguarding account, on the basis of DOMINO SAS’s instructions.
  6. Post-transaction monitoring by HawkAI:
    • After finalization of the transaction, HawkAI analyzes the transactions to detect any potential fraudulent activity. This adds an additional security layer to the process.

Cash Withdrawal from a Payment Account

This paragraph describes the process for an ATM (Distributeur Automatique de Billets, DAB) withdrawal made by the Client using the Card. Fees are charged by both DOMINO SAS and Arkéa.

Processing is performed within D+1 at most (business days), but the Client sees the Account balance decrease in real time from the Application.

  1. Client initiation of an ATM withdrawal:
    • The Client inserts the Mastercard Card into an ATM belonging to another bank, referred to as the “ATM Bank”.
    • The Client requests a cash withdrawal, enters the desired amount, and confirms the transaction.
  2. Authorization at the ATM:
    • The ATM, operated by the bank operating the ATM, connects to the Mastercard network for transaction authorization.
  3. Mastercard authorization:
    • Mastercard validates the Card details and forwards the authorization request to DOMINO SAS.
  4. DOMINO SAS authorization:
    • DOMINO SAS approves the withdrawal after checking the Client’s Account balance, withdrawal limits, and all internal fraud and risk rules.
  5. Mastercard clearing:
    • At the end of the batch period, Mastercard sends the clearing files.
  6. DOMINO SAS clearing:
    • DOMINO SAS reviews the clearing files and performs reconciliation.
  7. Mastercard settlement:
    • At the end of the batch period, Mastercard sends the settlement files.
    • Mastercard transfers funds to the bank operating the ATM.
    • Fees are added to the file, including an interchange fee of -0.2% sent to the bank operating the ATM.
  8. DOMINO SAS settlement:
    • DOMINO SAS reviews the settlement files and sends the order to Arkéa to transfer the funds to the settlement account.
  9. Arkéa:
    • Arkéa transfers funds from the safeguarding account to the settlement account.
    • Arkéa transfers funds from the settlement account to Mastercard.
  10. Bank operating the ATM:
  1. Client:
  1. Post-transaction monitoring by HawkAI:

Execution of a Card Payment Transaction

This paragraph describes the process for a Client Card payment at a Merchant’s point-of-sale terminal (POS). It involves the Merchant’s Bank, the Mastercard network for authorization and settlement, Arkéa for funds management, and DOMINO SAS for transaction authorization, recordkeeping, and communication with the Client. This ensures a secure and transparent payment experience for the Client.

Processing is performed within D+1 at most (business days), but the Client sees the Account balance decrease in real time from the Application.

It should be emphasized that, in the process described below, DOMINO SAS considers that the Merchant manages its own acquiring processing. This is not the general case, and an acquiring processor layer is inserted in most cases, but this point is not structural to the description of the process concerned.

  1. Initiation on the Merchant side:
    • The Client initiates a transaction at POS or on the internet.
    • For an online transaction, if the Merchant wishes, the Merchant starts a 3DS (3 Domain Secure) verification by contacting Mastercard:
      • Mastercard forwards the request to the ACS (Access Control Server) managed by Apata.
      • Apata then forwards the request to DOMINO SAS.
      • DOMINO SAS then forwards the request to the Client.
      • The Client validates the request.
      • The response is transmitted to Apata and then to the Merchant.
  2. The Merchant transmits the authorization request to Mastercard.
  3. Mastercard process:
    • The request is tokenized.
    • The request is transcribed into an ISO 8583 message.
    • The request is transmitted to the MIP (Message Integrity Protocol) server on CloudEdge.
    • The CloudEdge HSM containing the keys decrypts the message and transmits it to DOMINO SAS’s servers.
  4. DOMINO SAS:
    • Synchronous blocking sequence of checks:
      • PIN / PAN / other data checks.
      • Balance checks.
      • Limit checks.
      • MCC checks.
      • Preliminary fraud / AML / CTF check.
    • Non-blocking parallel sequence in the guaranteed execution engine:
      • Updating projected balances.
      • Adding the transaction to the ledger.
      • Returning validation to Mastercard.
  5. Mastercard:
    • The authorization is transmitted to the Merchant.
  6. Merchant:
    • The Merchant sends the authorized transactions to its Bank in a batch.
  7. Merchant’s Bank:
    • The Bank initiates clearing to request Mastercard to execute the transactions.
  8. Mastercard:
    • Mastercard transfers the clearing to DOMINO SAS for validation.
  9. DOMINO SAS:
    • DOMINO SAS validates the clearing and returns the result to Mastercard.
  10. Mastercard:
  1. DOMINO SAS:

Beneficiary Management

Transfer Beneficiaries correspond to a list managed by DOMINO SAS. This is therefore an internal process built around the User interface.

The app journey is:

  1. Home: the Client accesses account actions.
  2. Account actions: the Client chooses “Manage my Beneficiaries”.
  3. Beneficiary list: the Client may choose to edit, by clicking on the desired Beneficiary; add, using the button at the bottom of the screen; or delete, using the icon at the top of the screen, a Beneficiary.
  4. Editing an existing Beneficiary: the Client edits the Beneficiary’s last name / first name.
  5. Deleting an existing Beneficiary: the Client deletes a Beneficiary.
  6. Adding a Beneficiary: the Client adds a Beneficiary.
  7. Adding a Beneficiary - summary: the User accesses the summary.
  8. Adding a Beneficiary - code verification: the User must enter the User’s code to validate the addition.
  9. Adding a Beneficiary - validation of Beneficiary addition: the Client observes that the addition has been completed.

Execution of an SCT Transfer

This paragraph describes the process by which the Client initiates an SCT (SEPA Credit Transfer). This process involves DOMINO SAS, which transmits the transaction to Arkéa. Arkéa, as direct participant, processes the transaction through STEP2 (EBA Clearing) for clearing and settlement. DOMINO SAS manages communication with the client, transaction recording, and compliance, thereby ensuring a smooth and secure transfer process.

Processing is performed within D+1 at most and on the same day for a transfer requested before 12:00 (business days).

  1. Initiation:
    • The Client and the Beneficiary agree on the amount of the Transfer to be made.
    • The Client initiates a Transfer order in the Application.
  2. DOMINO SAS:
    • Synchronous blocking sequence of checks:
      • Balance checks.
      • Limit checks.
      • Preliminary fraud / AML / CTF check.
    • Non-blocking parallel sequence in Temporal’s guaranteed execution engine. Temporal receives workflows and guarantees that they will be fully performed. Without a positive response at each step, Temporal manages retries until success. It is necessary to monitor continuously to verify that there are no looping errors and to be able to resolve problems.
      • Updating projected balances.
      • Adding the transaction to the ledger.
      • Adding the transaction to the next batch file.
    • Synchronous operations at batch time:
      • Netting of amounts to be safeguarded / desafeguarded.
      • Sending the safeguarding / release from safeguarding/ring-fencing order to Arkéa.
      • Sending settlement files to Arkéa to operate transfers to beneficiaries’ accounts.
  3. Arkéa:
    • Desafeguarding of funds to Green-Got’s settlement account.
    • Sending clearing and settlement files to the CSM.
    • Sending funds to the CSM.
  4. CSM (EBA STEP2):
    • Clearing.
    • Sending funds to the Beneficiary’s Bank.
  5. Beneficiary’s Bank:
    • Receipt of funds and notification of the Beneficiary.
  6. Arkéa:
    • Sending the PSR (payment status report) to DOMINO SAS.
  7. DOMINO SAS:
    • Updating balances and the transaction ledger.
    • Notification of the Client.

Execution of an SCT Inst Transfer

The paragraph below describes the process by which the Client initiates an SCT Inst (SEPA Instant Credit Transfer) relying on TIPS (TARGET Instant Payment Settlement) as CSM (Clearing and Settlement Mechanism), which enables DOMINO SAS clients to make instant payments in EUR. DOMINO SAS manages acquisition and preparation of the payment order, Arkéa facilitates instant processing through TIPS, and DOMINO SAS provides real-time communication with the client and compliance. This system offers clients a fast, efficient, and secure way to execute their transactions.

  1. Initiation:
    • The Client and the Beneficiary agree on the amount of the transfer to be made.
    • The Client initiates a Transfer order in the Application.
  2. DOMINO SAS:
    • Synchronous blocking sequence of checks:
      • Balance checks.
      • Limit checks.
      • Preliminary fraud / AML / CTF check.
    • Non-blocking parallel sequence in the guaranteed execution engine:
      • Updating projected balances.
      • Adding the transaction to the ledger.
    • Synchronous blocking operations:
      • Sending the safeguarding / release from safeguarding/ring-fencing order to Arkéa.
      • Sending the transfer order to Arkéa.
  3. Arkéa:
    • Desafeguarding of funds to DOMINO SAS’s settlement account.
    • Sending the transfer order to the CSM.
    • Sending funds to the CSM.
  4. CSM (TIPS):
    • Sending funds to the Beneficiary’s Bank.
  5. Beneficiary’s Bank:
    • Receipt of funds and notification of the Beneficiary.
  6. Arkéa:
    • Sending the PSR (payment status report) to DOMINO SAS.
  7. DOMINO SAS:
    • Updating balances and the transaction ledger.
    • Notification of the Client.

Processing an SDD Core Where the Client Is the Debtor

In this scenario, DOMINO SAS’s role is to manage incoming SEPA direct debits debiting the Client’s Account opened in DOMINO SAS’s books, in accordance with the direct debit mandate, ensure smooth transaction processing, and maintain accurate records. The Client, as debtor, has control over direct debit mandates and can manage them directly through the Application.

Processing is performed within D+1 at most and on the same day for an SDD requested before 12:00 (business days).

  1. Client:
    • The Client, as debtor, signs a SEPA direct debit mandate in favor of the creditor. This mandate authorizes the creditor to debit amounts from the Client’s Account in its favor and instructs DOMINO SAS to allow those direct debits in accordance with that mandate.
    • The direct debit mandate must include a number of required statements, in particular the frequency and, if applicable, the amount of the direct debits.
  2. Creditor:
    • On the agreed collection dates, the creditor prepares a direct debit instruction in accordance with the rules of the SDD Core scheme. This instruction includes the details of the debtor’s DOMINO SAS account (the Client) and the amount to be collected.
    • The creditor submits this instruction to its bank.
  3. Creditor’s Bank:
    • The creditor’s bank processes the direct debit instruction and transmits it to the SEPA clearing system.
    • The clearing system transmits this instruction to DOMINO SAS, via Arkéa.
  4. DOMINO SAS:
    • Synchronous blocking sequence of checks:
      • Mandate verification.
      • Balance checks.
      • Limit checks.
      • Preliminary fraud / AML / CTF check.
    • Non-blocking parallel sequence in the guaranteed execution engine:
      • Updating projected balances.
      • Adding the transaction to the ledger.
      • Adding the transaction to the next batch file.
    • Synchronous operations at batch time:
      • Netting of amounts to be safeguarded / desafeguarded.
      • Sending the safeguarding / release from safeguarding/ring-fencing order to Arkéa.
      • Sending settlement files to Arkéa to operate transfers to beneficiaries’ accounts.
  5. Arkéa:
    • Desafeguarding of funds to Green-Got’s settlement account dedicated to SDD.
    • Sending clearing and settlement files to the CSM.
    • Sending funds to the CSM.
  6. CSM (EBA STEP2):
    • Clearing.
    • Sending funds to the creditor’s bank.
  7. Creditor’s bank:
    • Receipt of funds and notification of the creditor.
  8. Arkéa:
    • Sending the PSR (payment status report) to DOMINO SAS.
  9. DOMINO SAS:
    • Updating balances and the transaction ledger.
    • Notification of the Client.

Processing an SDD B2B Where the Client Is the Debtor

Processing is similar to SDD Core, except that the mandate requires systematic and stricter checks because it cannot be revoked.

The in-depth Hawk:AI checks are long and are performed only after most transactions. In the case of an SDD B2B, they are blocking and performed before the transactions are executed.

Acquisition of Payment Transactions

The process for acquiring payment transactions may be described as follows:

Closure of the Contractual Relationship

At the Client’s Initiative

This process was defined so that the Client is fully informed and autonomous throughout the Account closure process, while allowing DOMINO SAS to collect important information for continuous improvement of its services.

  1. Home:
    • The Client starts the process by accessing the Application home interface, where the Client finds an overview of the Account and the different available options.
    • From this home interface, the User must navigate to the personal profile, generally accessible through a specific menu or button.
  2. Client profile:
    • Once in the profile, the User searches for and selects the “personal information” option. This section contains personal information such as last name, first name, address, and other data related to the Account.
  3. Personal Information:
    • In personal information, the User identifies the option to initiate the Account closure procedure.
  4. Reasons for closure:
    • The User is invited to select or provide the reason for closing the Account. This choice may help DOMINO SAS understand the motivations behind this decision and improve its services.
  5. Conditions for closing the Account:
    • Before finalizing Account closure, a screen is displayed presenting the User with the conditions and implications of closing the Account. This may include information on possible fees, management of remaining balances, or the need to close specific products or services first.
    • The User must read and accept these conditions to continue.
  6. Final closure screen:
    • Once all previous steps have been completed and the conditions accepted, the User is directed to the final closure screen.
    • This screen may require final confirmation, such as entering a password or verification code, to validate Account closure.
    • After confirmation, a message from DOMINO SAS confirms the effective closure of the Account, and the User receives a summary email.
  7. Processing on the DOMINO SAS side:
    • Databases are updated.
    • The User’s data are archived for 10 years for any request from regulatory authorities; they will then be automatically destroyed. They have a TTL (time to live) in the database.
    • The commercial relationship is closed instantly at the end of the process.
At DOMINO SAS’s Initiative

If irregular use of the Green-Got Account is detected, contrary to the general terms and conditions, such as fraud, money laundering, or terrorist financing, DOMINO SAS immediately initiates the closure process above.

The visual process describes the following steps:

  1. Trigger: confirmed fraud, money laundering, or terrorist financing.
  2. Initialization of the closure process, where applicable with an email requesting bank details (RIB) in the Client’s name.
  3. Preparation of a Suspicious Activity Report (SAR).
  4. Collection of KYC information and extraction of transaction history.
  5. Sending of the SAR dossier.
  6. Recording the date on which the SAR was sent and retaining the Client’s data for 10 years.

In addition:

Payment Institution Carrying Out Hybrid Activities

Does your institution carry out or plan to carry out other commercial activities in the next three years?

(Article L.522-3, I of the French Monetary and Financial Code: “Without prejudice to the provisions of III of Article L.522-8, payment institutions may carry out, on a regular professional basis, an activity other than the provision of payment services, subject to the legislative and regulatory provisions applicable to that activity.

For these payment institutions carrying out hybrid activities […]“)

YesNo

As indicated in the preceding sections, DOMINO SAS will allow its Clients to subscribe to other regulated products and services, outside Payment Services, thereby exposing DOMINO SAS to qualification as an Institution carrying out hybrid activities:

These 25 funds are divided into four portfolios, each corresponding to different risk levels. They are offered to clients based on a knowledge questionnaire established before subscription. Contracts are accessible from an initial subscription of EUR 500. All operations related to the contract, including payment, redemption, portfolio change, etc., can be managed directly from the Application.

Ancillary Services

(Article L.522-2, I of the French Monetary and Financial Code)

Ancillary serviceSelected
Scriptural foreign exchange services
Custody, data recording, and data processing services
Guarantee of execution of payment transactions

DOMINO SAS will not provide ancillary services.

Use of Agents

(Articles L.523-1 to L.523-6 of the French Monetary and Financial Code)

DOMINO SAS will not use Agents.

Exercise of Activity Abroad

(Articles L.526-21 et seq. of the French Monetary and Financial Code)

Activities Under Freedom to Provide Services or Freedom of Establishment

Indicate whether the exercise of payment services activities under freedom to provide services or freedom of establishment in another State of the European Economic Area is envisaged.

YesNo

DOMINO SAS plans to carry out its activities under Freedom to Provide Services in all the following EEA countries:

In accordance with ACPR instructions for preparing the freedom to provide services dossier, a single form was completed for all the countries above, since the payment services provided in each country are identical. That form is provided in the annexes (5.1 LPS Form).

DOMINO SAS does not plan, within a three-year horizon, to carry out its activities under freedom of establishment in another EU / European Economic Area Member State.

Activities Outside the European Economic Area

Indicate whether the exercise of payment services activities in States not belonging to the European Economic Area is envisaged.

YesNo

DOMINO SAS does not plan to carry out its activities outside the European Economic Area.

Business Plan

Market Study

General Market Analysis

The environment as a major concern for French women and men:

Despite geopolitical and economic uncertainty, the environment and related issues, in particular climate change, biodiversity and pollution, remained the 3rd-ranked main concern of the French population in September 20233. This topic was cited by 32% of respondents among their three main concerns.

This level of concern is shared by the population as a whole but is more present among people under 35 and higher socio-professional categories (CSP+), which are the main targets of the Green-Got offering proposed by DOMINO SAS.

Visual summary of image55.png:

Concern cited among the three main personal concernsTotal
Purchasing power40
Personal health and the health of close relatives37
The environment, including climate change, biodiversity and pollution32
Incivility and crime32

The same Ipsos study evidences a strong desire among French people to become more involved in environmental issues, since 80% consider that minimizing their personal impact is “Important”. A very large majority already take daily action.

Finance is an indispensable lever for succeeding in the environmental transition and a rapidly expanding market:

To meet its environmental objectives by 2030, the European Union will need at least EUR 700 billion per year through 20304. These amounts will be allocated to changes in production methods, transport, and the ways goods and services are consumed.

To meet this ever-growing financing demand, the sustainable finance market is rapidly expanding.

In a study published in August 2023, Polaris Market Search estimated the revenue associated with the global sustainable finance market at USD 5,000 billion, with average annual growth of 19.9% between 2023 and 2032, supported by the following factors:

Consumers are ready to act and to pay for sustainable financial services, but they are not satisfied with their banking services on environmental matters:

According to a McKinsey study published in April 2023, 39% of U.S. consumers would be ready to subscribe to financial services linked to climate issues.

Many say they are ready to pay more for financial services linked to these issues, particularly in savings, investment and advisory services, provided that the impact of these services is measurable and demonstrable. 38% would accept a price increase of around 20%, and 27% say they are ready for an increase of up to 60%.

Nevertheless, as suggested by the chart below from the same study, they are generally disappointed by the actions of the Credit Institutions (Établissements de Crédit) of which they are customers, and their perception of those actors’ actions is judged rather “Insufficient”:

Visual summary of image76.png: the McKinsey chart states that banks are not yet able to stand out based on their green pledges or credentials. It compares consumer perceptions of their banks on whether the bank is a sustainable brand, makes a difference for the environment, affects climate change for the better, and has a clear environmental message. The exact average points are not labeled in the extracted visual, but the visual supports the dossier’s statement that satisfaction is low and that no analyzed institution exceeded a 59% satisfaction threshold on any question.

Worse still, none of the institutions analyzed exceeded the 59% satisfaction threshold, even on a single question.

The opportunity is therefore immense for new actors such as DOMINO SAS and its Green-Got offering to capture these market shares in France and then in Europe:

The banking penetration rate in France is very high, since French people hold more than 73 million bank accounts5, including 6.6 million for Professionals (Professionnels)6, representing an average of 1.3 bank accounts per inhabitant, based on excluding those under 15.

Assuming alignment between the expectations and behaviors of French consumers and U.S. consumers, it is possible to estimate the total addressable market for sustainable bank accounts at 26 million accounts for Individuals (Particuliers) and 2.6 million for Professionals, including Micro-Enterprises.

For the other European Union member states, extrapolation and a similar calculation make it possible to envisage the sustainable bank account market at 134 million accounts, based on 343 million accounts7 opened in the European Union.

Growing appetite for innovative financial services at the expense of traditional actors:

Many Fintechs appeared in Europe in the mid-2010s with the ambition of challenging the positions of Credit Institutions and other major financial actors. Initially focused overall on the payments sector, these Fintechs now wish to strengthen their attractiveness and develop new sources of revenue by expanding their offerings to other types of financial services, such as savings or credit. With varied statuses or authorizations, these actors are pursuing autonomy once market proof has been obtained, and some have begun the steps required to become Credit Institutions.

These Fintechs have already attracted tens of millions of customers within the European Union and hundreds of millions worldwide, for a total market estimated at EUR 67 billion in 20228, which is expected to grow by 55% per year on average through 2030 and thereby exceed USD 2,000 billion by that horizon.

With 29% market share, these actors are best established in the European Union plus the United Kingdom, with very substantial Individual customer portfolios:

Professional customers are an essential growth driver for these actors in terms of number of customers and profitability:

Despite a generally more difficult fundraising context for Fintech actors, investors and institutional actors remain very interested in innovative financial-sector actors, as shown by the following:

“Climate Techs” are increasingly sought after by investors, especially “Climate Fintechs”:

Despite a downturn in 2023 linked to the economic context, Climate Techs demonstrated their attractiveness by raising more than EUR 13 billion in Q1 202311, bringing the total funds raised by these actors to USD 117 billion since 2020, resisting the financing crisis far better than traditional start-ups.

Finally, in 2022, Climate Fintechs in Europe raised nearly USD 1 billion, up 10% compared with 2021 despite the difficult economic context.12

Sustainable financial services are expanding rapidly in the United States and Europe but are still lagging in France:

Many “sustainable” financial services have been launched within the European Union and the United Kingdom in recent years and have achieved significant success. This is the case in particular for the following actors:

Outside Europe, the success of Aspiration in the United States is particularly significant, with more than 5 million customers.

In France, by contrast, the traditional actors present in the “sustainable” or “ethical” financial services market have not been able to demonstrate real commercial traction:

Positioning of DOMINO SAS

DOMINO SAS and its Green-Got offering position themselves in relation to several categories of actors:

Strengths and weaknesses of traditional banking actors in the sustainable financial services market:

Strengths and weaknesses of actors specialized in sustainable development in the sustainable financial services market:

Strengths and weaknesses of generalist Fintechs in the sustainable financial services market:

Strengths and weaknesses of Impact Fintechs in the sustainable financial services market:

How Green-Got differentiates itself from these different types of competitors:

DOMINO SAS develops its Green-Got services offering with awareness of the different strengths and weaknesses of actors present in the market, which translates into the following initiatives and approaches:

To differentiate itself from traditional actors:

The surrounding source states that this practice is illustrated by one of the three most recent events organized by DOMINO SAS to meet its Customers in Paris.

To differentiate itself from Fintechs:

Benchmark of offerings and features - Individuals (actors specialized in sustainable development / Impact Fintechs):

CriterionGreen-GotCrédit CoopératifLa NefBunqTriodos BankTandem BankTomorrowHeliosGoodvest
Countries presentFrance, BelgiumFranceFranceNetherlands, Germany, France, Italy & SpainGermany, Spain, United Kingdom & NetherlandsUnited KingdomGermanyFranceFrance
Number of customers19,500100,00080,0009m740,000280,000120,00017,5005,000
Launch dateJune 2022189319862012198020212 (as shown in source visual)201720212021
Payment account / current accountYesYesNo, business onlyYesYesNoYesYesNo
Preferred segmentIndividuals aged 25-40AssociationEnterprisesIndividuals aged 25-40Individuals of all agesIndividuals of all agesIndividuals of all agesIndividuals of all agesIndividuals aged 25-40
Customer typeIndividual; Enterprise (Q1 2025); Micro-EnterpriseAssociation; Individual; Enterprise; Micro-EnterpriseEnterprise; Individual; Enterprise; Micro-EnterpriseIndividual; Enterprise; Micro-EnterpriseIndividual; Enterprise; Micro-EnterpriseIndividualIndividualIndividualIndividual
Monthly fee for payment servicesEUR 6 incl. VAT (TTC)EUR 3.7 incl. VAT per monthN/aEUR 17.99 incl. VATFrom EUR 5.5N/aEUR 7 then EUR 8 in March 2023 incl. VATEUR 6 incl. VATN/a
French IBANYesYesYesYesNoNoNoNoN/a
SEPA transferFree and unlimitedFree and unlimitedFree and unlimitedFree and unlimitedFree and unlimitedN/a30 per month30 per monthN/a
Round-up to finance impact projectsYesYesNoNoNoN/aNoNoN/a
Customer Service6/7 telephone & email6/7 telephone & email6/7 telephone & email6/7 by email6/7 by email6/7 by email5/7 by email5/7 by email5/7 by email
Foreign-currency paymentFree and unlimited2.9% of amountN/aFree and unlimitedFree and unlimitedN/a1% of amount1% of amountN/a
Customer ratings4.9/51.8/54.3/5Not shown3.9/54.3/54.7/54.3/54.9/5
CO2 footprint calculation for expensesYesNoNoNoNoN/aYesNoN/a
Apple & Google PayYesYesN/aYesYesN/aYesNoN/a
Piggy bank & vaultYesNoN/aYesNoN/aYesYesN/a
Donation to NGOs at each paymentYesYesNoYesNoN/aYesNoN/a
Carbon intensity of deposits145 t/mEUR121 t/mEUR121 t/mEURUnknownUnknownN/aUnknown148 t/mEURN/a
Savings & investment productLife insurance (Q1 2024); passbooks (Q4 2024)Life insurance; passbooksPassbooksEquivalent passbooksEquivalent passbooks & securities accountEquivalent passbooks & securities accountEquivalent passbooks & securities accountNoLife insurance

Benchmark of offerings and features - Sole Traders (generalist Fintechs):

CriterionGreen-GotQontoShineBlank
Countries presentFranceFrance, Spain, Italy & GermanyFranceFrance
Number of customers1,000400,000150,00020,000
Launch dateApril 2023201620172021
Preferred segmentMicro-EnterpriseSME (PME)Micro-EnterpriseMicro-Enterprise
Monthly fee for payment servicesEUR 5 excl. VAT (HT)EUR 9 excl. VATEUR 7.9 excl. VATEUR 6 excl. VAT
SEPA transferFree and unlimited30 per month30 per month30 per month
Round-up to finance impact projectsYesYesNoNo
Customer Service6/7 telephone & email7/7 chat & email7/7 telephone & email7/7 chat & email
Foreign-currency paymentFree and unlimitedEUR 1 + 1.9% of amount2% of amount1.9% of amount
Customer ratings4.9/54.7/54.7/54.3/5
CO2 footprint calculation for expensesYesNoNoNo
Apple & Google PayYesYesYesYes
Piggy bank & vaultYesNoN/aNo
Donation to NGOs at each paymentYesNoNoNo
Pre-accounting toolsQ1 2025YesNo, not on this offerYes

Revenue and Cost Model

Given its history and trajectory, DOMINO SAS will structure its business model around several lines of activity, structured by the applicable regulatory framework:

DOMINO SAS’s revenue model will therefore rely on the combination of the following revenue lines:

DOMINO SAS’s expense model will rely mainly on variable expenses, thereby facilitating adaptation of the system to the growth pace of the future Payment Institution.

The main expense items will be:

The other expense items will be:

Expenses linked to the partnership with PPS EU SA will represent 10% of Operating Expenses in N1 and 0.5% in N2.

DOMINO SAS has chosen to internalize as much as possible the functions necessary to conduct its activity as a Payment Institution. Consequently, DOMINO SAS has decided to progressively establish a substantial team of 34 FTEs in IT & Product by year N5, for a total of 27% of payroll, i.e. 7% of total operating expenses. As a result, other expenses linked to IT and its outsourcing will remain measured and will reach only 3% of total Operating Expenses in year N5, compared with 2% in year 1.

The other expenses envisaged in the Business Plan, including various subscriptions and tools, come from different quotes and/or draft contracts provided by the different service providers.

Finally, investments mainly of a technical nature will be made as part of the authorization process and over the period covering years N1 to N5 of the Business Plan, including Mastercard Principal Membership, obtaining a BIN, and mobile payment configuration for Apple Pay and Google Pay. These investments will be amortized over five years.

Annual Financial Statements for the Last 3 Financial Years

The audited financial statements of DOMINO SAS for 2020, 2021 and 2022 are provided in annex:

It should be noted that the first accounting period of DOMINO SAS lasted 18 months, from June 2020 to December 2021.

Forecast Annual Accounts

Central Scenario

Given the filing timetable of this Authorization Dossier, the forecast annual accounts and associated indicators are projected according to the following assumptions:

Historical data are based on the following:

Supported by its model and offering adapted to the needs of its targets, DOMINO SAS expects sustained growth in the number of customers between years N1 and N5.

DOMINO SAS will deploy targeted communication campaigns to grow the size of its customer portfolio:

The acquisition strategy described in Paragraph 5.1.2.2 for broadening the customer base will be pursued, with particular attention paid to the following axes:

Improvement and addition of new functionalities will also strengthen customer acquisition, in particular:

DOMINO SAS customer projections (central scenario), end of period:

Customer segmentN-4 2020 & 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total customers / segment total-10,07721,52533,34867,797100,713145,308198,651264,200
Individual Customer - Standard-10,07720,59727,42844,76659,83878,100100,044126,807
Micro-Enterprise - Standard--9282,3085,9509,89615,53523,00933,184
Individual Customer - Premium---2,75510,20316,75324,38733,24243,749
Micro-Enterprise - Premium---8573,0795,6449,39914,30320,890
Enterprise - Standard----2,8416,39813,41421,01329,570
Enterprise - Premium----9582,1864,4737,04010,000

DOMINO SAS’s economic model is based on:

Forecast evolution of DOMINO SAS revenue (central scenario):

Revenue lineN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total revenueEUR 353,207EUR 1,102,068EUR 1,868,339EUR 8,199,518EUR 16,112,470EUR 28,654,130EUR 46,524,416EUR 72,139,212
SubscriptionsEUR 296,585EUR 933,010EUR 1,406,585EUR 5,437,698EUR 9,419,227EUR 16,143,282EUR 24,518,950EUR 36,575,064
Other payment-account revenueEUR 6,933EUR 18,335EUR 35,286EUR 134,288EUR 210,375EUR 330,699EUR 488,389EUR 668,225
InterchangeEUR 49,690EUR 146,400EUR 291,280EUR 1,406,758EUR 3,852,988EUR 7,321,837EUR 12,562,215EUR 19,622,779
Investment revenueEUR -EUR 4,323EUR 135,188EUR 1,220,774EUR 2,629,880EUR 4,858,312EUR 8,954,863EUR 15,273,144
Total split100%100%100%100%100%100%100%100%
Subscription split84%85%75%66%58%56%53%51%
Other payment-account revenue split2%2%2%2%1%1%1%1%
Interchange split14%13%16%17%24%26%27%27%
Investment revenue split0%0%7%15%16%17%19%21%

DOMINO SAS projections provide for regular growth in its “Revenue” (Chiffre d’affaires), at a sustained pace, from EUR 8M in year 1 to EUR 72M in year N5, through:

The monthly subscription amount by account type will evolve between N-1 and N5 as shown below:

Monthly subscription excl. VAT (HT), in EURN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Individual Customer - Standard5.005.005.005.005.006.006.007.00
Micro-Enterprise - Standard-5.007.007.007.008.008.009.00
Individual Customer - Premium--10.0010.0012.0012.0012.0013.00
Micro-Enterprise - Premium--12.0012.0014.0014.0014.0015.00
Enterprise - Standard---35.0035.0035.0035.0035.00
Enterprise - Premium---50.0050.0050.0050.0050.00

Revenue linked to interchange commissions will remain the same until year N5 under the following assumptions:

Revenue linked to activities will remain the same until year N5 with:

DOMINO SAS’s expense model:

The main expense assumptions of DOMINO SAS are:

DOMINO SAS staffing:

Forecast evolution of DOMINO SAS staffing (central scenario), end of period in number of FTEs:

FTE categoryN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total FTEs, end of period1627355684113143173
Admin & support functions13369131516
IT & Product710121622263134
Marketing45689111316
Customer Support2551121344256
Compliance2461219243643
Internal Control & Risks--334568

Forecast evolution of DOMINO SAS staffing (central scenario), annual average in number of FTEs:

FTE categoryN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total FTEs, annual average112028417095129158
Admin & support functions12348111516
IT & Product48111319242833
Marketing34579101216
Customer Support135716263748
Compliance135815203239
Internal Control & Risks--023467

Forecast income statement of DOMINO SAS (central scenario), amounts in EUR:

Line itemN-4 2020 & 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Payment activity revenue (Agent then PI)77,863353,2071,097,7451,733,1516,978,74513,482,59123,795,81837,569,55356,866,068
Intermediation activity revenue--4,323135,1881,220,7742,629,8804,858,3128,954,86315,273,144
Total revenue77,863353,2071,102,0681,868,3398,199,51816,112,47028,654,13046,524,41672,139,212
Banking system-167,631509,4181,086,2072,506,6262,630,4544,242,5626,059,8358,330,268
Personnel expenses270,673467,1671,075,0131,715,2872,724,4104,567,3656,457,0449,358,53912,169,455
Marketing & Acquisition-69,555264,254788,3623,059,7915,148,4109,017,22215,065,89226,539,640
Other expenses177,000303,036451,786704,4292,184,2412,959,7774,282,1576,452,4339,435,199
Depreciation, amortization and provisions (DAP)-46,750121,448207,687462,192698,824943,1041,252,9481,626,022
Total expenses447,6731,054,1392,421,9194,501,97210,937,25916,004,83024,942,08938,189,64858,100,584
Operating result-369,810-700,931-1,319,851-2,633,633-2,737,741107,6413,712,0428,334,76814,038,628
Financial income---92,750160,000102,00056,00070,00070,000
Financial expenses---44,295103,66077,65468,43952,34835,684
Financial result---48,45556,34124,346-12,43917,65234,316
Corporate income tax(92,453)(175,233)(329,963)(646,295)(670,350)32,997924,9011,131,7103,518,236
Tax rate25%25%25%25%25%25%25%25%25%
Corporate income tax deficit carryforward(92,453)(267,685)(597,648)(1,243,942)(1,914,293)(1,881,296)(956,395)00
Other taxes (CII, CIR)35,67569,92880,00090,000100,000----
Net result after tax-334,135-777,723-1,239,851-2,495,178-2,581,401131,9873,699,6037,220,71010,554,708
Cumulative result-334,135-1,111,858-2,351,709-4,846,887-7,428,287-7,296,301-3,596,6983,624,01214,178,720

Forecast balance sheet of DOMINO SAS (central scenario), amounts in EUR:

Asset line itemN-4 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Intangible fixed assets6,809312,672378,042680,1041,167,2091,547,2952,017,0752,546,9183,141,146
Tangible fixed assets-22,51118,50020,28785,165132,519159,965228,061306,122
Equity interests and related receivables---------
Other financial fixed assets13330,14030,14030,1403,294,9955,947,8039,837,12114,941,92321,682,800
Fixed assets6,942365,323426,682730,5314,547,3697,627,61712,014,16117,716,90225,130,068
Stocks---------
Operating receivables-11,280-------
Other receivables62,982186,584241,757------
Intercompany receivables---------
VAT and other taxes--80,00090,000100,000----
Cash and cash equivalents204,975526,5084,740,8902,452,9289,661,6207,555,9227,767,12612,099,38320,216,713
Prepaid expenses78,94438,58771,947------
Current assets346,901762,9595,134,5942,542,9289,761,6207,555,9227,767,12612,099,38320,216,713
Total assets353,8431,128,2825,561,2763,273,45914,308,98915,183,53919,781,28729,816,28445,346,781
Liability line itemN-4 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Share capital17,63217,63224,81024,81024,81024,81024,81024,81024,810
Share premiums518,2251,305,3916,171,9616,171,96119,671,96119,671,96119,671,96119,671,96119,671,961
Legal reserve---------
Retained earnings-(335,069)(1,112,792)(2,487,643)(4,999,621)(7,562,991)(7,309,802)(3,322,124)4,246,493
Result(335,069)(777,723)(1,374,851)(2,511,978)(2,563,370)253,1893,987,6787,568,61711,248,900
Investment grant-128,370-------
Equity200,788338,6013,709,1281,197,15012,133,78012,386,96916,374,64723,943,26435,192,164
Provisions for risks and charges---------
Financial debts92,214489,0471,660,0001,613,1251,340,0001,215,000835,000595,000305,000
Short-term financial debts---------
Suppliers16,391157,573136,805395,481713,2601,390,3612,308,1403,587,9975,653,358
Other debts50018,12555,34367,702121,949191,210263,500350,508446,625
VAT and other taxes43,950124,836-----1,339,5153,749,633
Deferred income---------
Total liabilities353,8431,128,1825,561,2763,273,45914,308,98915,183,53919,781,28729,816,28445,346,781

Degraded Scenario

The degraded scenario is based on the following assumptions, expressed as differences from the central scenario:

This degradation will significantly affect DOMINO SAS revenue, with Revenue in year N5 down -32% compared with the central scenario, reaching EUR 55M compared with EUR 72M. The efforts to diversify the offering will be maintained in order to increase revenue per customer, which will reach EUR 258 in year N5 compared with EUR 273 in the central scenario.

DOMINO SAS’s expense model will allow it to break even in N4, thanks to the following levers:

G&A, Marketing & Communication, Tools & IT expenses will be maintained at the central-scenario level in order to preserve:

The investment assumptions linked to DOMINO SAS’s activity amount to a total of EUR 480K and break down as follows:

DOMINO SAS customer acquisition assumptions:

DOMINO SAS customer projections (degraded scenario), end of period in K Customers:

Customer segmentN-4 2020 & 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total customers / segment total-10,07721,52530,35057,07382,865118,096160,402212,536
Individual Customer - Standard-10,07720,59725,45138,52350,07564,27581,496102,633
Micro-Enterprise - Standard--9282,0094,8928,02312,51818,48326,610
Individual Customer - Premium---2,2058,16213,40419,51526,59735,005
Micro-Enterprise - Premium---6852,4624,5137,52211,44716,720
Enterprise - Standard----2,2635,09710,68416,73923,560
Enterprise - Premium----7711,7533,5835,6398,008

DOMINO SAS’s revenue model:

Forecast evolution of DOMINO SAS revenue (degraded scenario):

Revenue lineN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total revenueEUR 353,207EUR 1,179,750EUR 1,747,012EUR 6,836,757EUR 12,795,204EUR 22,329,911EUR 35,663,433EUR 54,753,281
SubscriptionsEUR 296,585EUR 933,010EUR 1,306,655EUR 4,595,766EUR 7,687,078EUR 13,055,871EUR 19,718,926EUR 29,350,122
Other payment-account revenueEUR 6,933EUR 27,034EUR 33,459EUR 115,515EUR 173,156EUR 268,059EUR 393,051EUR 536,028
InterchangeEUR 49,690EUR 215,383EUR 271,711EUR 1,131,476EUR 2,919,143EUR 5,310,880EUR 8,742,749EUR 13,091,863
Investment revenueEUR -EUR 4,323EUR 135,188EUR 993,999EUR 2,015,827EUR 3,695,100EUR 6,808,707EUR 11,775,268

DOMINO SAS staffing:

Forecast evolution of DOMINO SAS staffing (degraded scenario), end of period:

FTE categoryN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total FTEs, end of period1627355575105130153
Admin & support functions13369131516
IT & Product710121622263134
Marketing45689111316
Customer Support2551116303543
Compliance2461116203038
Internal Control & Risks--333566

Forecast evolution of DOMINO SAS staffing (degraded scenario), annual average:

FTE categoryN-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Total FTEs, annual average112028406789121144
Admin & support functions12348111516
IT & Product48111319242833
Marketing34579101216
Customer Support135714233239
Compliance135814172834
Internal Control & Risks--023456

Forecast income statement of DOMINO SAS (degraded scenario), amounts in EUR:

Line itemN-4 2020 & 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Payment activity revenue (Agent then PI)77,863353,2071,175,4271,611,8245,842,75710,779,37718,634,81028,854,72642,978,013
Intermediation activity revenue--4,323135,188993,9992,015,8273,695,1006,808,70711,775,268
Total revenue77,863353,2071,179,7501,747,0126,836,75712,795,20422,329,91135,663,43354,753,281
Banking system-167,631562,7571,046,4532,228,2652,058,1273,322,5214,734,6256,308,645
Personnel expenses270,673467,1671,075,0131,715,2872,684,9884,362,1715,976,5378,731,62710,983,186
Marketing & Acquisition-69,555399,254773,2582,899,2194,719,0878,144,23913,494,96123,995,646
Other expenses177,000303,036451,786664,4502,002,2272,689,0223,884,9745,871,6188,580,824
Depreciation, amortization and provisions (DAP)-46,750121,448224,487460,541684,841934,0861,239,9321,565,092
Total expenses447,6731,054,1392,610,2574,423,93610,275,23914,513,24822,262,35734,072,76351,433,394
Operating result-369,810-700,931-1,430,508-2,676,924-3,438,482-1,718,04567,5541,590,6693,319,887
Financial income---92,750160,000102,00056,00070,00070,000
Financial expenses---44,295103,66077,65468,43952,34835,684
Financial result---48,45556,34124,346-12,43917,65234,316
Corporate income tax(92,453)(175,233)(357,627)(657,117)(845,535)(423,425)13,779402,080838,551
Tax rate25%25%25%25%25%25%25%25%25%
Corporate income tax deficit carryforward(92,453)(267,685)(625,312)(1,282,429)(2,127,965)(2,551,389)(2,537,611)(2,135,531)(1,296,980)
Other taxes (CII, CIR)35,67569,92880,00090,000100,000----
Net result after tax-334,135-777,723-1,350,508-2,538,469-3,282,142-1,693,69955,1151,608,3213,354,203
Cumulative result-334,135-1,111,858-2,462,366-5,000,835-8,282,976-9,976,675-9,921,560-8,313,239-4,959,036

Forecast balance sheet of DOMINO SAS (degraded scenario), amounts in EUR:

Asset line itemN-4 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Intangible fixed assets6,809312,672378,042680,1041,167,2091,547,2952,017,0752,546,9183,141,146
Tangible fixed assets-22,51118,50020,28783,514118,536150,947215,046261,993
Equity interests and related receivables---------
Other financial fixed assets13330,14030,14030,1402,379,2294,152,8606,657,6149,771,33413,673,543
Fixed assets6,942365,323426,682730,5313,629,9515,818,6918,825,63612,533,29717,076,681
Stocks---------
Receivables62,982197,864197,864------
VAT and other taxes--80,00090,000100,000----
Cash and cash equivalents204,975526,5084,790,37715,925,2459,770,5516,340,2603,837,3962,596,5462,903,971
Prepaid expenses78,94438,58771,947------
Current assets346,901762,9595,140,18816,015,2459,870,5516,340,2603,837,3962,596,5462,903,971
Total assets353,8431,128,2825,566,87016,745,77613,500,50312,158,95112,663,03215,129,84319,980,652
Liability line itemN-4 2021N-3 2022N-2 2023N-1 2024N1 2025N2 2026N3 2027N4 2028N5 2029
Share capital17,63217,63224,81024,81024,81024,81024,81024,81024,810
Share premiums518,2251,305,3916,171,96119,671,96119,671,96119,671,96119,671,96119,671,96119,671,961
Legal reserve---------
Retained earnings-(335,069)(1,112,792)(2,463,300)(5,001,769)(8,283,910)(9,977,609)(9,922,494)(8,314,173)
Result(335,069)(777,723)(1,350,508)(2,538,469)(3,282,142)(1,693,699)55,1151,608,3213,354,203
Investment grant-128,370-------
Equity200,788338,6013,733,47114,695,00211,412,8609,719,1629,774,27711,382,59814,736,801
Provisions for risks and charges---------
Financial debts92,214489,0471,641,2501,613,1251,300,0001,145,000775,000595,000305,000
Short-term financial debts---------
Suppliers16,391157,573136,805369,946667,3491,120,9091,867,1532,827,1274,539,049
Other debts50018,12555,34367,702120,293173,880246,604325,118399,802
VAT and other taxes43,950124,836-------
Deferred income---------
Total liabilities353,8431,128,1825,566,87016,745,77613,500,50312,158,95112,663,03315,129,84319,980,652

Initial Capital

DOMINO SAS currently has share capital of EUR 24,809.70, which will be increased by EUR 13.5 million as part of a fundraising planned in 2024, to reach total Equity of EUR 14.7 million upon obtaining the Payment Institution authorization (N-1).

Consequently, DOMINO SAS will satisfy the minimum capital requirement provided for by regulation.

Prudential Own Funds Requirements

(Article 29 of the Order of 29 October 2009)

The method chosen for calculating own funds is as follows:

  1. Method A ☐

  2. Method B ☒

  3. Method C ☐

We calculated the own funds requirement using Method B, in accordance with the regulation applicable to Payment Institutions for calculating own funds.

For comparison, we also performed this calculation using Methods A and C. These three methods can be consulted in the Annex (5.2 Business Plan - central scenario and 5.3 Business Plan - degraded scenario).

We also estimated additional own funds needs made necessary by the hybrid nature of the future Payment Institution DOMINO SAS, in accordance with requests made during the ACPR meeting of 10/11/2023:

Own funds are composed as follows:

According to the different calculations, Methods A, B and C, relating to own funds requirements linked to payment services and to the hybrid nature of DOMINO SAS as a future Payment Institution, DOMINO SAS will have an own funds surplus compared with the calculated requirement, both under the central scenario and the degraded scenario, with regard to the forecast accounting balance sheets available in the annex (5.2 Business Plan - central scenario and 5.3 Business Plan - degraded scenario).

Calculation of Own Funds requirements according to Methods A, B & C (central scenario):

ScenarioMethodMetricN1 2025N2 2026N3 2027N4 2028N5 2029
CentralMethod AOwn funds requirementEUR 1,091,923EUR 1,588,363EUR 2,465,401EUR 3,763,394EUR 5,717,499
CentralMethod AHybrid activity requirementEUR 319,556EUR 371,739EUR 503,088EUR 860,841EUR 1,440,107
CentralMethod ATotal own funds requirementEUR 1,411,479EUR 1,960,102EUR 2,968,489EUR 4,624,234EUR 7,157,606
CentralMethod AOwn funds after deductionEUR 13,529,941EUR 10,586,485EUR 10,369,894EUR 13,827,729EUR 20,802,119
CentralMethod AOwn funds surplus / deficitEUR 12,118,463EUR 8,626,384EUR 7,401,404EUR 9,203,495EUR 13,644,512
CentralMethod BOwn funds requirementEUR 668,038EUR 1,594,932EUR 2,135,932EUR 2,592,042EUR 3,206,646
CentralMethod BHybrid activity requirementEUR 319,556EUR 371,739EUR 503,088EUR 860,841EUR 1,440,107
CentralMethod BTotal own funds requirementEUR 987,594EUR 1,966,671EUR 2,639,020EUR 3,452,882EUR 4,646,753
CentralMethod BOwn funds after deductionEUR 13,529,941EUR 10,586,485EUR 10,369,894EUR 13,827,729EUR 20,802,119
CentralMethod BOwn funds surplus / deficitEUR 12,542,348EUR 8,619,814EUR 7,730,873EUR 10,374,847EUR 16,155,365
CentralMethod COwn funds requirementEUR 2,818,725EUR 3,208,955EUR 3,827,749EUR 4,277,087EUR 4,752,991
CentralMethod CHybrid activity requirementEUR 319,556EUR 371,739EUR 503,088EUR 860,841EUR 1,440,107
CentralMethod CTotal own funds requirementEUR 3,138,281EUR 3,580,694EUR 4,330,837EUR 5,137,927EUR 6,193,098
CentralMethod COwn funds after deductionEUR 13,529,941EUR 10,586,485EUR 10,369,894EUR 13,827,729EUR 20,802,119
CentralMethod COwn funds surplus / deficitEUR 10,391,661EUR 7,005,791EUR 6,039,056EUR 8,689,802EUR 14,609,020

Calculation of Own Funds requirements according to Methods A, B & C (degraded scenario):

ScenarioMethodMetricN1 2025N2 2026N3 2027N4 2028N5 2029
DegradedMethod AOwn funds requirementEUR 1,027,524EUR 1,451,325EUR 2,226,236EUR 3,407,276EUR 5,143,339
DegradedMethod AHybrid activity requirementEUR 294,427EUR 287,338EUR 314,317EUR 433,279EUR 644,557
DegradedMethod ATotal own funds requirementEUR 1,321,951EUR 1,738,663EUR 2,540,553EUR 3,840,555EUR 5,787,897
DegradedMethod AOwn funds after deductionEUR 13,527,793EUR 9,865,566EUR 7,702,087EUR 7,227,359EUR 8,241,452
DegradedMethod AOwn funds surplus / deficitEUR 12,205,843EUR 8,126,903EUR 5,161,534EUR 3,386,804EUR 2,453,555
DegradedMethod BOwn funds requirementEUR 538,468EUR 1,314,086EUR 1,801,101EUR 2,185,632EUR 2,585,502
DegradedMethod BHybrid activity requirementEUR 294,427EUR 287,338EUR 314,317EUR 433,279EUR 644,557
DegradedMethod BTotal own funds requirementEUR 832,894EUR 1,601,424EUR 2,115,419EUR 2,618,911EUR 3,230,059
DegradedMethod BOwn funds after deductionEUR 13,527,793EUR 9,865,566EUR 7,702,087EUR 7,227,359EUR 8,241,452
DegradedMethod BOwn funds surplus / deficitEUR 12,694,899EUR 8,264,142EUR 5,586,668EUR 4,608,448EUR 5,011,393
DegradedMethod COwn funds requirementEUR 2,750,565EUR 3,046,763EUR 3,518,089EUR 4,015,642EUR 4,439,340
DegradedMethod CHybrid activity requirementEUR 294,427EUR 287,338EUR 314,317EUR 433,279EUR 644,557
DegradedMethod CTotal own funds requirementEUR 3,044,992EUR 3,334,101EUR 3,832,406EUR 4,448,920EUR 5,083,898
DegradedMethod COwn funds after deductionEUR 13,527,793EUR 9,865,566EUR 7,702,087EUR 7,227,359EUR 8,241,452
DegradedMethod COwn funds surplus / deficitEUR 10,482,801EUR 6,531,465EUR 3,869,681EUR 2,778,439EUR 3,157,555

Protection of Collected Funds and Safeguarding Rules

(Articles L.522-17 of the Monetary and Financial Code and 34 of the Order of 29 October 2009)

A safeguarding account / ring-fenced account (compte de cantonnement) will be opened in the name of DOMINO SAS with the Credit Institution Arkéa (see annex 4.5 Arkéa letter for support with Green-Got’s authorization application).

Daily safeguarding and release from safeguarding (cantonnement et décantonnement) operations for DOMINO SAS Customer funds will be performed by the DOMINO SAS teams.

The Safeguarding of Funds Procedure is provided in annex (7.3.7 Procédure de cantonnement des fonds).

The only persons with access to DOMINO SAS’s safeguarding account will be:

Acquisition or Participation Projects

To date, DOMINO SAS has no acquisition or participation project for the next three years.

Organizational Structure

General Organization Chart of DOMINO SAS

Governance of DOMINO SAS

The organization chart and governance of DOMINO SAS were built to comply with the separation rules between operational functions and control functions.

The organization chart takes the following form:

Visual summary of image85.png:

As indicated in Section 4, the governance of the Payment Institution will be composed of the following functions/bodies:

Organization and Organization Chart of DOMINO SAS

Visual summary of image62.png:

Governance / reporting areaRoles and teams shown
Supervisory BoardTop-level body above the Strategic Committee, Management Committee, and Audit and Risk Monitoring Committee
ManagementA. GANOVELLI, General Manager / CEO, Effective Manager 1; M. CAILLAUX, President / CMO, Effective Manager 2; F. HUET, CTO
Risk and Internal ControlK. BOUSRY, Head of Risk & Internal Control, shown alongside the management structure
ComplianceHead of Compliance, recruitment in progress; 2 FTE Compliance Manager; 3 FTE Compliance analyst
Customer Care & OpsA. THOMAS; 1 FTE Customer Care Manager Investment Specialist; 1 FTE Customer Care Investment Specialist; 3 FTE Customer Care operator
Support functionsA. CRASATO, People Officer; 1 FTE CFO; A. BAULARD, Impact Officer
GrowthC. JAUNAULT; 1 FTE CRM Manager; 1 FTE Growth Engineer
ContentC. LeFranc; 1 FTE Content; 1 FTE Community Manager; 1 FTE Content & design
Product Management / Design1 FTE Product Manager; 1 FTE Product Designer; 1 FTE Assistant Product Manager
Development2 FTE mobile app development; 5 FTE back-end development

Under the responsibility and direct management of Mr A. GANOVELLI, General Manager and Effective Manager:

Under the responsibility and direct management of Ms M. CAILLAUX, President and Effective Manager:

Under the responsibility and direct management of Mr F. HUET, CTO:

Under the responsibility and direct management of Mr K. BOUSSRY, Head of Risks & Internal Control:

Use of Outsourcing

Visual summary of image13.png:

Area shown in visualServices / partners
Essential outsourced services (Prestations essentielles externalisées)Identity verification: Ubble; production, personalization and shipping of cards: Exceet; PEP and asset-freeze screening: Efficiale; Pay-in card processing: Mastercard Send and Checkout; transaction monitoring redundancy: Hawk:AI; card-payment acquisition: Stripe; hosting: AWS, Scaleway, Exoscale, Cloudflare, Hetzner; regulatory reporting: Invoke; periodic control: in progress
Internal Payment Institution functionsOnboarding; customer relationship management; AML/CFT scoring and decisions; relationship follow-up including KYC updates, transaction monitoring, alert/decision management and suspicious-transaction reports; compliance; risk management; internal control; account opening and management; card issuance and closing; payment/card management; card authorization management; operations; accounting; marketing/communication; customer relationship management
Banking servicesSEPA lead institution: Arkéa; safeguarding account (Compte de Cantonnement): Arkéa
Other non-essential servicesPayroll management: PayFit; legal secretariat: external lawyer; accounting/tax: accountant (ECAI)

1. So-called “Essential” Services (PSEE)

DOMINO SAS will use outsourcing for the performance of the following essential services, within the meaning of Articles 231 et seq. of the Order of 03 November 2014:

The associated draft contracts can be consulted in annexes 4.5 to 4.14.

For each PSEE, DOMINO SAS has designated an internal Manager responsible for daily monitoring of the service:

2. Other Services

The other outsourced services, which do not qualify as essential services within the meaning of Articles 231 et seq. of the Order of 03 November 2014, are:

Participation in a National or International Payment System

DOMINO SAS will participate in the following payment systems:

Statutory Auditors

DOMINO SAS will rely on BDO France for the performance of statutory audit (Commissariat aux Comptes, CAC) missions. The annex therefore includes BDO Paris’s registration certificate on the CAC list (6.1.1 Attestation d’inscription sur la liste des CAC de BDO Paris), its KBis extract (6.1.2 Extrait KBis de BDO Paris) and the engagement acceptance letter (6.1.3 Lettre d’acceptation de mandat de BDO Paris).

Internal Control Mechanisms

Responsible Persons and Committees

Head of the second-level permanent control function (contrôle permanent de deuxième niveau): Kaïss BOUSRY.

(Article 16 of the Order of 03 November 2014)

Mr Bousry’s CV is in annex (6.2.1 CV de M. BOUSRY), as well as his identity document (6.2.2 Pièce d’identité de M. BOUSRY).

Effective manager responsible for the coherence and effectiveness of permanent control: Andréa GANOVELLI.

(Article 16 of the Order of 03 November 2014)

Head of the internal audit function:

DOMINO SAS wishes to outsource the internal audit function to a specialized firm. The firm is being selected.

(Articles 17 of the Order of 03 November 2014)

Effective manager responsible for the coherence and effectiveness of periodic control (contrôle périodique): Maud CAILLAUX.

(Article 17 of the Order of 03 November 2014)

Risk Committee: Yes ☒ No ☐

Audit Committee: Yes ☒ No ☐

The Audit and Risk Committee will correspond to a single body at DOMINO SAS and will meet at least monthly.

The role and functions of each Manager and Committee involved in the internal control system are described in detail in the Internal Control and Risk Management Procedure in the annex (7.1.1 Procédure de contrôle interne et de gestion des risques).

As a reminder, the actors in internal control are:

Risk Mapping

DOMINO SAS’s risk map (cartographie des risques) was prepared in accordance with Articles 100 to 103 of the Order of 3 November 2014. It appears in annex (7.1.2 Cartographie des risques).

3. Methodology

The methodology for DOMINO SAS’s risk map appears in DOMINO SAS’s Internal Control and Risk Management Procedure, available in the annex (7.1.1 Procédure de contrôle interne et de gestion des risques).

The main steps are:

4. Permanent and Periodic Control Plans Associated with the Risk Map

DOMINO SAS has prepared a permanent control plan listing the controls performed:

This plan is closely linked to the risk map and is being finalized.

In addition, Level 1 and Level 2 permanent controls are defined and formalized in Control Sheets. The annex includes the template for this Control Sheet and an example of a completed Control Sheet (7.1.3 Modèle de Fiche de définition et de formalisation des contrôles permanents).

DOMINO SAS has chosen to outsource the internal audit function, responsible for periodic control, to a specialized firm currently being selected. Once selection is finalized, the firm and Green-Got will meet to finalize the multi-year periodic control plan.

5. Description of Risks

DOMINO SAS’s risk map mainly brings out four major types of risks to which Green-Got is exposed.

Legal Risk

In accordance with Article 10 k) of the Order of 3 November 2014, legal risk is the risk linked to any dispute with a counterparty resulting from any inaccuracy, gap or deficiency likely to be attributable to DOMINO SAS in respect of its operations.

Examples:

Non-Compliance Risk

In accordance with Article 10 p) of the Order of 3 November 2014, non-compliance risk is the risk of judicial, administrative or disciplinary sanction, significant financial loss or reputational damage arising from failure to comply with provisions specific to banking and financial activities, whether legislative or regulatory, national or directly applicable European provisions, professional and ethical standards, or instructions from the Effective Managers issued in particular pursuant to the orientations of the supervisory body.

Examples:

Operational Risk

In accordance with Article 10 j) of the Order of 3 November 2014, operational risk is the risk of losses arising from inadequacy or failure of internal processes, personnel and systems, or from external events.

Examples:

DOMINO SAS has subscribed to Professional Civil Liability Insurance (Assurance Responsabilité Civile Professionnelle, ARCP), covering in particular operational risks.

Financial Risk

Financial risk results from a lack of control over the company’s financial and economic management, resulting either from endogenous factors, including business model, organization and operational efficiency, or exogenous factors, including macro-economic factors.

Example:

Evolution of Internal Control Staffing

The organization of DOMINO SAS’s internal control system makes it possible to have an adequate, autonomous and responsible organization: compliant with the activities performed, capable of rapid decision-making, capable of ensuring effective control and supervision of all activities, and capable of steering and controlling all risks, all within a culture of transparency and compliance.

Internal control and risk management are placed under the responsibility of Mr BOUSRY.

At the launch of the activity, the staffing dedicated to internal control will be:

The staffing dedicated to internal control will increase over the next 6 financial years of DOMINO SAS, in line with the projected workload, namely:

Organization of the Internal Control System

The organization of the internal control system is precisely described in the Internal Control and Risk Management Procedure included in the annex (7.1.1 Procédure de contrôle interne et de gestion des risques).

DOMINO SAS’s internal control and risk management system is characterized by all means that make it possible to ensure that operations performed, the organization and the procedures established comply with:

In addition, DOMINO SAS’s internal control and risk management system also makes it possible to verify:

DOMINO SAS’s internal control and risk management system is based on five fundamental principles:

DOMINO SAS’s internal control and risk management system is therefore composed of the following elements:

Accounting Procedures

DOMINO SAS’s financial statements are prepared according to the accounting principles applicable to Payment Institutions in France and follow the General Chart of Accounts (Plan Comptable Général, PCG), as permitted by the applicable regulation.

The Effective Managers of DOMINO SAS will be responsible for the truthfulness and integrity of the financial statements.

The accounting firm ECAI prepares the accounting and financial statements, and Mr Maurice Soued, Manager of the firm, signs the annual accounts.

The annual accounts are audited by the Statutory Auditor BDO Paris.

The accounting year will cover the period from 1 January to 31 December.

DOMINO SAS’s Accounting Procedure is in the annex (7.3.1 Procédure comptable).

Selection and Control of PSEE

DOMINO SAS distinguishes outsourced services called “essential” services (PSEE, Prestataires de Services Essentiels Externalisés) from outsourced services called “non-essential” services.

DOMINO SAS will use outsourced services classified as essential and has therefore implemented a PSEE Selection and Control Policy, included in the annex (7.1.4 Politique de sélection et de contrôle des PSEE). This policy specifies the selection process and the control arrangements applicable to PSEE.

In general, the process for selecting an “essential” Partner is as follows:

DOMINO SAS also implements a governance and control framework for outsourced services.

The Outsourced Functions Register is included in the annex (7.1.5 Registre des fonctions externalisées).

AML/CFT / Asset Freezing / Fight Against Tax Fraud

Responsible Persons

Name of the person responsible for implementing the AML/CFT framework (dispositif LCB-FT): Head of Compliance, recruitment in progress.

Name of the TRACFIN correspondent: Head of Compliance, recruitment in progress.

Name of the TRACFIN reporting officer: Head of Compliance, recruitment in progress.

The Head of Compliance job description is included in the annex (6.3.1 Fiche de poste du Head of Compliance).

ML/TF Risk Classification

In accordance with Article L.561-4-1 of the CMF, DOMINO SAS identified, assessed and formalized the money laundering and terrorist financing (BC-FT) risks to which it is exposed in an ML/TF risk classification (7.2.1 Classification des risques de BC-FT).

In accordance with Article L.561-4-1 of the CMF, this classification is based on risks relating to:

It is updated at least annually. An early review also occurs after any event significantly affecting DOMINO SAS’s activities, customer base or establishments, such as the launch of a new product, a regulatory change, opening of a new customer segment, or significant ML/TF risks.

The following risk factors were identified.

Risks Relating to Customer Characteristics

DOMINO SAS has no occasional customers. Use of the services necessarily requires creation of an account and acceptance of the General Terms and Conditions, which materialize the establishment of a business relationship.

DOMINO enters into business relationships with the following customers:

DOMINO SAS systematically refuses any business relationship with:

Risks Relating to Products and Services Offered

DOMINO SAS offers the following services:

To limit ML/TF risk, ceilings in the form of technical blocks are associated with some of these services according to customer typology.

None of these services presents a low ML/TF risk. Customers, their representatives and, where applicable, beneficial owners are systematically identified and their identity is verified at onboarding into the business relationship.

Risks Relating to the Terms or Specific Conditions of Transactions

To assess ML/TF risks relating to the terms and conditions of execution of transactions or attempted transactions by customers, DOMINO SAS takes into account the following criteria:

These criteria are specified in the ML/TF risk classification.

Risks Relating to Distribution Channels

Because onboarding is performed remotely, DOMINO SAS uses the Ubble channel, now Check-Out.

In this context and to limit as much as possible the risk around use of this channel, Green-Got collects:

Risks Relating to Countries

DOMINO SAS refers to the country list attached as an annex to the risk classification, cross-checked against the list used by hawk.ai, DOMINO SAS’s transaction monitoring platform.

The country-risk implementation is as follows:

Organization of the AML/CFT, Asset-Freezing and Tax-Fraud Framework

The organization of the AML/CFT, asset-freezing and tax-fraud framework is detailed in the AML/CFT and Asset-Freezing Compliance Framework Policy included in the annex (7.2.2 Politique cadre conformité de LCB-FT et de Gel des avoirs).

Governance

Supervisory Board

The Supervisory Board meets as often as DOMINO SAS’s interests require and at least once every six months.

It is responsible for DOMINO SAS’s compliance with its internal-control and AML/CFT / asset-freezing obligations.

Its responsibilities in this respect are:

To perform its AML/CFT / asset-freezing duties effectively, the Supervisory Board must have direct access to the Compliance Officer’s activity reports, permanent control reports from the Head of Risks and Internal Control, periodic control reports from the internal audit function, ACPR findings, TRACFIN communications and declarations, and similar information.

Effective Managers

The two Effective Managers of DOMINO SAS are responsible for the institution’s compliance with the applicable AML/CFT / asset-freezing regulations and, consequently, for compliance with the AML/CFT Policy and all related procedures.

Their responsibilities are:

Audit and Risk Committee

DOMINO SAS’s Audit and Risk Committee meets once per month. The Head of Risks and Internal Control or the person responsible for Compliance may, however, ask the Committee Chair to convene the Committee for a defined agenda.

For AML/CFT / asset freezing, its responsibilities are:

First Line of Defense

Head of Compliance

The Head of Compliance of DOMINO SAS is being recruited.

For AML/CFT, the Head of Compliance acts as the person responsible for implementing AML/CFT / asset-freezing frameworks within the meaning of Article L.561-32 of the CMF. The Head of Compliance’s responsibilities are detailed precisely in the AML/CFT and Asset-Freezing Compliance Framework Policy.

Operational Staff

Operational staff involved in AML/CFT / asset freezing mainly belong to the Compliance function. Operational staff belonging to Customer Care may also intervene in AML/CFT because they are in direct contact with customers.

The effectiveness of the AML/CFT framework depends on compliance with know-your-customer obligations from onboarding into the relationship and on ongoing due-diligence obligations throughout the business relationship.

Compliance operational staff implement AML/CFT / asset-freezing frameworks and apply the procedures in force under the supervision of the Head of Compliance.

Their responsibilities are detailed precisely in the AML/CFT and Asset-Freezing Compliance Framework Policy.

Second Line of Defense

The Head of Risks and Internal Control is responsible for permanent control.

The Head of Risks and Internal Control also acts as the person responsible for permanent control of AML/CFT / GDA frameworks within the meaning of Article 15 of the Order of 6 January 2021.

The permanent-control officer therefore coordinates the institution’s permanent-control framework, including second-level controls, which:

The Head of Risks and Internal Control’s AML/CFT responsibilities are detailed precisely in the AML/CFT and Asset-Freezing Compliance Framework Policy.

Third Line of Defense

The internal audit function for AML/CFT / GDA frameworks verifies the effectiveness and appropriateness of permanent control in this area.

As permitted by the applicable regulation, DOMINO SAS chose to entrust periodic control to an external provider.

Summary Presentation of the AML/CFT, Asset-Freezing and Tax-Fraud Framework

As part of its AML/CFT / asset-freezing framework, DOMINO SAS implements the following measures prescribed by the Regulation, using a risk-based approach.

During onboarding into the business relationship:

During the business relationship:

These measures are described in detail in the operating policies and procedures included in the annex:

To date, DOMINO SAS enters into relationships only with Natural-Person Customers. The onboarding process for legal persons is therefore being built. Some of the above procedures may be amended as part of that work.

Procedure for security systems and access to sensitive data

Collection of statistical data relating to performance, operations and fraud

Type of data collected

Identification data for natural persons

As part of KYC, DOMINO SAS collects the following data for its activity as a Payment Institution (Établissement de Paiement) and for its activity as an agent of PPS EU SA.

Mandatory data

Natural-person customers (Clients Personnes Physiques) must provide the following mandatory information:

  1. Personal identification information

a. Full name (first name and surname);

b. Date and place of birth;

c. Nationality;

d. Photo (via Ubble for PVID identification);

e. Video (via Ubble for PVID identification).

  1. Identity document

a. Passport (data and image);

b. Or identity card (data and image);

c. Or residence permit (data and image).

  1. Contact details

a. Full residential address;

b. Telephone number;

c. Email address;

d. Country of tax residence.

  1. Additional information for AML-CFT (LCB-FT)

a. Risk scoring of the business relationship;

b. Politically Exposed Person (Personne Politiquement Exposée) status;

c. Presence on sanctions lists.

  1. Situation

a. Occupation;

b. Income level.

DOMINO SAS does not consider any of this data to be sensitive payment data within the meaning of PSD2 (DSP2).

Optional data

As part of a review for a customer with a high score or suspicious movements, DOMINO SAS may request additional data.

  1. Contact details

a. Proof of address;

b. Tax identification number.

  1. Situation

a. Income tax notice;

b. Property/local tax notice;

c. Property title.

DOMINO SAS does not consider any of this data to be sensitive payment data within the meaning of PSD2.

Identification data for legal persons

For each legal person (Personne Morale), DOMINO SAS must be able to identify at least one natural-person contact, who is subject to the same information requests as natural-person customers. With respect to the company itself, DOMINO SAS requests the following data:

  1. Company identification information

a. Name of the company or organization;

b. Legal form (SARL, SA, etc.);

c. Address of the registered office and, where applicable, other establishments or branches;

d. Registration number in the Trade and Companies Register (Registre du Commerce et des Sociétés, RCS) or equivalent;

e. Company creation date;

f. SIREN / SIRET.

  1. Tax identification

a. Tax identification number;

b. VAT number.

  1. Information on managers and persons empowered to manage or represent the company

a. Names, dates of birth, nationalities and addresses of managers (CEO, managing directors, directors);

b. Copies of identity documents for managers and persons authorized to bind the company if they are not onboarded as natural persons.

  1. Shareholding and ownership structure

a. Identity of the main shareholders or beneficial owners (for example, natural persons holding more than 25% of the share capital or voting rights);

b. Shareholding and control structure of the company.

  1. Company information

a. Nature of the business activity (NAF);

b. Description of the company’s main activity.

In the event of a compliance review following a suspicion, additional information may be requested, such as:

  1. Company articles of association;

  2. Minutes of general meetings and other important legal documents;

  3. Insurance certificates or other regulatory documents;

  4. Company accounts;

  5. Kbis extract.

DOMINO SAS does not consider any of this data to be sensitive payment data within the meaning of PSD2.

Login data

The login data stored are:

DOMINO SAS does not consider any of this data to be sensitive payment data within the meaning of PSD2.

Transaction data

Data specific to SEPA transactions (Sct, SCT Inst, Sdd, Sdd B2B):

Data specific to card transactions:

Common data:

Monitoring data

To be able to monitor transactions, DOMINO SAS collects the following data:

6. Principles of statistical data collection

Collection is performed in real time, at each login or new operation. All collection is performed by DOMINO SAS tools:

The data are accessible in real time through a dashboard available to the IT department and to fraud and AML-CFT operators. The reported data are as follows:

ONBOARDING: weekly data

TRANSACTION MONITORING: weekly data

ACCOUNT MANAGEMENT: weekly data

These data allow DOMINO SAS to track the evolution of the fraud rate and the relevance of the actions taken to combat identity theft, fraud and AML-CFT.

These data are accessible to the technical teams, the Customer Service function and the Compliance function.

Summaries are anonymized.

7. System operation

Several security measures have been implemented within the technical infrastructure and through the processes and procedures used to manage DOMINO SAS IT systems.

The Information Systems Security Policy available as an annex (7.3.5 Information Systems Security Policy) provides the general framework for security operation.

Business continuity management, detailed in the PUPA available as an annex (7.3.6 Emergency and Business Continuity Plan), provides the general framework for the business continuity plan.

Regarding access to data centers, it should be emphasized that DOMINO SAS does not have access to the servers used. They are managed hosting at AWS.

Backup management is detailed in the Information Systems Security Policy available as an annex (7.3.5 Information Systems Security Policy), which describes backup frequency and how backups are operated:

For example, an “administrator” may add or remove an employee from a “manager customer care” group. Once in this group, the manager may add another employee to the “customer care” group. The employee will then have read and write rights to the data corresponding to the rights of the employee’s group. Each employee may belong to several groups.

Procedure for monitoring, handling and following up security incidents and customer complaints

Incident management covers any event that disrupts or is capable of disrupting a service. This includes:

The GDPR has required all companies to report certain types of personal data breaches to the competent national supervisory authority and, as a result, these data breaches are now formally classified as incidents.

An incident occurs when the operational state of a production system moves from a functioning state to a failed state, or is about to do so, resulting in a state in which the system does not operate as it was designed or implemented. Resolving an incident involves implementing a repair to restore the item to its original state.

The Incident Management Procedure (7.3.4 Incident Management Procedure) and the Complaints Management Procedure (7.3.2 Complaints Management Procedure) are included as annexes.

Identity of the person(s) or bodies responsible for supporting customers in cases of fraud, technical incidents or complaint management

Customer Service is responsible for supporting customers in cases of fraud, technical incidents or complaints. It is therefore responsible for all reported incidents concerning systems used by customers and is in particular responsible for the following tasks:

Incidents reported by customers are handled by DOMINO SAS Customer Service. The customer’s complaint may be made by telephone, by email or through the online contact form on DOMINO SAS’s Green-Got website (customer area).

The Customer Service agent records the complaint in incident.io. If the agent identifies a malfunction, the agent forwards the request to the supervisor, who creates a ticket in linear (ticket tracking and project management software), specifying the type of problem encountered and the criticality of the incident.

Depending on the type of problem, the support ticket is assigned to the appropriate owner within the IT team.

The incident is handled by the IT team with support from third-party suppliers if necessary. The owner of the relevant scope within the IT team confirms resolution in the linear ticket.

Customer Service is available to respond to any customer request during the incident resolution period.

Contact point for customers (name and email address) and procedures for escalating fraud within the DOMINO SAS organization

The contact point for customers in case of fraud will be Customer Service, which the customer may contact via:

Customer Service will be the contact point for customers, in particular in the following cases (most frequent cases):

The procedures for escalating fraud within the DOMINO SAS organization are detailed in the Anti-Fraud Procedure available as an annex (7.3.3 Anti-Fraud Procedure).

The handling of each of these problems is based on a process established internally by Customer Service, thereby enabling responses to customer questions, requests and complaints.

The chat and messaging system are open to receive messages 24/7/365, but a response is guaranteed only within 12 working hours.

Incident follow-up is performed by the Fraud and Complaints Officer, whose role is to identify confirmed cases of fraud, ensure the impact of this fraud on service customers is assessed and ensure that the customer, the Supervisory Board and external bodies are properly informed where applicable.

Organizational measures and fraud-prevention tools

The organizational measures and fraud-prevention tools are detailed in the Anti-Fraud Procedure available as an annex (7.3.3 Anti-Fraud Procedure).

DOMINO SAS’s approach to fraud prevention is risk-based. DOMINO SAS uses a number of reporting and monitoring tools that help prevent fraud at a level considered acceptable by the Supervisory Board:

The Head of Compliance may be alerted to suspicious activity in two ways:

The Head of Compliance reviews:

Incident reporting procedures

The Incident Management Process, including incident reporting, is described in detail in the dedicated procedure included as an annex (7.3.4 Incident Management Procedure).

The RSSI and the IT team responsible for infrastructure record all confirmed incidents in incident.io. Monthly, quarterly and annual reports on observed incidents are generated by incident.io and sent to DOMINO SAS’s monthly Audit and Risk Committee.

Quarterly reports are sent to the DOMINO SAS Supervisory Board.

Requests relating to personal data are recorded by the DOMINO SAS DPO. In the event of an identified security failure affecting personal data, the DPO reports the incident to the CNIL within 48 hours of identification, organizes corrective measures and manages information to the affected customers. If an incident is reported to the CNIL, the DOMINO SAS Supervisory Board is informed immediately.

Monitoring tools used and follow-up measures / procedures in place to mitigate security risks

In the context of anti-fraud measures, the internal Back Office and Hawk:AI are the tools used by DOMINO SAS for the prevention and mapping of fraud typologies.

These tools make it possible to trace flows (Who activates? Who has the same IP / same telephone? Who uses the same device?). This mapping enables anti-fraud profiling.

The Back Office tool is used by DOMINO SAS for screening against PEP and asset-freeze lists. It is combined with an Efficiale API connection.

Metabase is data analysis and collection software that allows data to be viewed and analyzed quickly and efficiently in near real time. DOMINO SAS uses it for many purposes, including inventory tracking, shipment tracking, order tracking and shipments.

Several possible fraud cases have already been identified by DOMINO SAS:

It should also be emphasized that other fraud cases not yet envisaged at this stage may occur and will be integrated into the tools used after their detection by DOMINO SAS.

Different teams within DOMINO SAS will be involved in managing these fraud cases, namely:

Procedures for restricting, recording, monitoring and tracing access to sensitive payment data

DOMINO SAS has implemented a comprehensive suite intended to secure access to its entire Information System, in accordance with the Information Systems Security Policy included as an annex (7.3.5 Information Systems Security Policy).

The procedure implemented for managing authorizations and access to sensitive data is intended to protect and control access to sensitive data contained in internal and external databases, access to equipment, physical access to premises, and to make it possible to determine at a given point in time and after the fact who was authorized and for what.

The main components of DOMINO SAS’s authorization management policy are as follows:

Description of data flows classified as sensitive payment data

As part of its activity, DOMINO SAS will collect, store and transmit payment data considered sensitive to the various IS applications:

Operations or processes involving sensitive data:

OperationDescription and associated sensitive payment data
Creation of current account (and KYC)Collection and recording of personal data linked to the identity of the person => from the user to the server via the application
Collection and recording of the person’s contact details => from the user to the server via the application
Creation and recording of payment data for the account created (IBAN) => internal to DOMINO SAS servers
Updating customer-side personal dataCollection and recording of personal data linked to the identity of the person => from the customer to the server via the application
Opening life insurance or savings accountCollection and recording of personal data linked to the identity of the person => from the customer to the server via the application
These data are shared with DOMINO SAS partners (Generali for life insurance and CFCAL for savings accounts) => from DOMINO SAS servers to partner servers
Consultation and modification of data in the Back OfficeDOMINO SAS employees have, depending on their accreditation, read and write access to customers’ personal identification data so they can assist customers in using the product or conduct analyses on transactions => from DOMINO SAS servers to the Back Office application through the VPN
Creation and manufacture of a payment cardCreation and recording of card data (PAN, PIN, CVC, token, expiration and cardholder name) => internal to DOMINO SAS servers
Transmission of these data to the card manufacturer (Exceet) => from DOMINO SAS servers to the manufacturer’s servers
Transmission of these data to the payment scheme (Mastercard) => from DOMINO SAS servers to the scheme’s servers
Reading and transmission of account data to the customerWhen the customer asks to display account characteristics or issue bank account details / IBAN (RIB / IBAN) => from DOMINO SAS servers to the customer’s application
In the Back Office, when data reading is requested by authorized personnel => from DOMINO SAS servers to the Back Office application through the VPN
Reading, writing and transmission of card dataWhen the customer wants to view card data in the application => from DOMINO SAS servers to the customer’s application
When the customer wants to update card data in the application (valid only for the PIN) => from the customer to the server via the application
3DS challengeDuring a 3DS request from a merchant, the entire chain allows DOMINO SAS to transmit the challenge to the customer and the response to the ACS => ACS -> DOMINO SAS -> Customer -> DOMINO SAS -> ACS

Personal data linked to identity

Personal data linked to identity are collected when a customer account is created and when these data are modified by the customer themselves or by authorized DOMINO SAS personnel:

Personal data linked to identity are as follows:

Data collected from the website, mobile application or CRM are transmitted to other applications and external service providers to ensure the proper functioning of DOMINO SAS processes and activity:

These data are structured differently depending on DOMINO SAS’s activity - Payment Institution or agent of PPS EU SA - due to differences within the associated IT infrastructure (including differentiated databases), but the data are identical in terms of content.

Whatever the DOMINO SAS activity (Payment Institution or agent of PPS EU SA), all applications, servers and partners allow these data to be processed in the following ways:

For fraud prevention, DOMINO SAS has implemented a step to secure customer areas, requiring double authentication of the customer during any new creation or modification of customer-account data.

Data relating to payment instruments or the transaction

Data relating to payment instruments or a transaction include all data collected or handled:

Data relating to payment instruments or a transaction cover the following data types:

Data collected from the website, mobile application or CRM are transmitted to other applications and external service providers to ensure the proper functioning of the processes and business:

Nature of possible operations on data relating to payment instruments or the transaction:

Nature of operationsCustomer applicationsBack OfficeDOMINO SAS servers and databasesHawk:AIMastercard / ArkéaGenerali / CFCAL
CollectionYesYesYesYesYesYes
RecordingYesYesYesYes
Reading or consultationYesYesYesYesYesYes
StructuringYesYesYes
ModificationYesYesYes
UseYesYesYes
TransmissionYesYesYesYesYes
RetentionYesYesYesYes
DeletionYesYesYes

Point of attention:

Login data

Login-related data are collected:

Login data are collected by Hawk:AI (fraud-detection tool) and are not stored by the website outside the IP address used during onboarding.

Login-related data cover:

Card data

As part of the activities carried out by DOMINO SAS, the payment card is identified by a PAN (Primary Account Number) and a public token.

The full PAN, expiration date, CVC and PIN are stored in a PCI DSS compliant part of the Information System and accessible only with high-level rights:

The public token makes it possible to identify the card in the rest of the IS and to display information such as the truncated PAN and the cardholder name.

The PAN is essential to operate payment transactions. The only possible operation from a public token is to link a transaction to a customer and an account.

The full PAN, expiration date, CVC and PIN are transmitted securely to Exceet and Mastercard when the card is created or modified (only for the PIN).

Description of internal and/or external access rights to collected sensitive payment data

Sensitive payment data can be accessed from several entry points:

Access to data by the customer:

The request-login and authentication process is detailed in the section “Customer device authentication procedure”. It ensures the security of access to sensitive payment data.

Sensitive data accessible to the customer are the data associated with the customer’s card and IBAN:

Access to data by DOMINO SAS personnel:

DOMINO SAS personnel must apply the Authorization Management Policy in force:

Participants in the authorization management process:

It should be noted that only three people will have access to sensitive payment data. The authorization management process is therefore simplified.

FunctionRole in the process
RSSIValidates specifications:
The procedures, actors and roles;
The authorization management process.
Ensures that authorizations are duly transferred in the event of departure or replacement;
Receives modification requests;
Verifies authorization compliance.
Lead Back EndDirects and controls implementation of the business logic that restricts access for any person or service that is not authorized.
Lead OpsDirects and controls implementation of the infrastructure that restricts hardware access for any person or service that is not authorized, in particular by installing a sealed barrier between the databases hosting sensitive payment data and the others;
Writes authorizations under the control of the RSSI.
RCCIAccesses the AWS audit account to audit all access and all modifications to the database(s) hosting sensitive payment data.

Single user repository:

All employees are recorded in the single user repository. This is a database that contains administrative data for all users who have a contractual relationship with the company (internal, external, interns, temporary workers and consultants), as well as the groups to which they are attached and have been attached.

Access to this repository is through the DOMINO SAS back office according to the user’s authorizations.

Update information for internal and external employee data is entered by managers according to a defined and shared process. User identifiers are defined as follows:

DOMINO SAS personnel must use the secure IT equipment and software provided by the institution. Personal IT equipment is therefore not authorized.

DOMINO SAS has no internal network. Access to IS resources is via VPN, using the user’s identifier and an OTP generated by an authenticator installed on the user’s mobile device.

Repository of authorizations and access rights by application:

Users have access only to the hardware, programs, folders and files they need to perform their operational tasks.

Authorizations are not attached to users but to groups. A user may be added to several groups. The user has access to the union of all access rights of the groups to which the user is attached.

Group membership is part of users’ rights. For example, a person in the “manager customer care” group may add or remove a user from the “employee customer care” group.

In the event of an employee’s departure, transfer or change in tasks and responsibilities, managers remove the employee from the group, and the employee’s access rights are therefore revoked.

General policy for regular control of authorizations and permissions:

To ensure the reliability and completeness of the user repository and maintain the level of IS security over time, a regular control of authorizations and permissions is performed.

This quarterly control is triggered and verified by the RCCI, but is largely automatic.

It is performed in 4 steps:

  1. Extract data to generate a complete mapping of groups and their members in a single file;

  2. Ask each manager to fill in a form indicating which group the members of the manager’s team should belong to, supplemented by automatic generation of the complete mapping of groups and their members in a single file;

  3. Compare these two files to verify that there are no discrepancies;

  4. Request correction of discrepancies from the managers concerned if necessary.

This review makes it possible to identify incorrect function assignments and authorization errors.

Description of tools for monitoring sensitive payment data and access

Because sensitive payment data are stored in a part isolated from the rest of the IS and access is highly restricted, monitoring is simplified.

The access-monitoring tool for sensitive payment data is the AWS audit account. The RCCI has access to an account and must check each quarter that no access has been made by anyone other than the three authorized persons and the server. The RCCI must also perform a frequency analysis and, in the event of an anomaly, escalate it to the RSSI for investigation.

Read access to sensitive payment data by a user must be an exception. The frequency threshold for consulting payment data that triggers a justification request is therefore 1 per quarter.

The same applies to write access. The only possible reason for write access to these data is a production incident requiring manual correction by one of the three authorized persons. The RCCI must find the documented record of this incident in incident.io (the incident management tool).

Description of archiving arrangements for collected data

Technical backup management is fully outsourced to AWS.

Backup frequency in replicas is continuous. It is based on a trigger system on databases. Each time data is recorded in the production database, it is also recorded in the replicas. If the production database fails, the first available replica becomes the production database.

Backup frequency in archives is multiple:

Description of internal and/or external use rights for collected sensitive payment data

The persons authorized to modify group access rights are the three IS administrators:

Applications used in data flows considered sensitive payment data:

Internal applications are accessible only via the VPN.

Connection to internal applications is also based on groups and their authorizations. For each entity in a database, access may be granted to a group to:

Point of attention: only functionalities or operations relating to sensitive data are presented below, application by application.

Customer-accessible applications (web and mobile):

Application roleEnable end customers to use the capabilities of their account.
UsersCustomers.
Authorization-determination ownerAuthorizations are requested by Product Managers according to customer needs and validated by the RSSI.
Access managerIS administrators.
Access typeReading data;
Launching the data-creation process (during a transfer request or account creation);
Launching the data-modification process (PIN modification request).

Internal back office:

Application roleEnable DOMINO SAS employees to manage internal processes.
UsersDOMINO SAS employees.
Authorization-determination ownerAuthorizations are discussed during specification workshops between the various stakeholders and validated by the RSSI.
Access managerIS administrators for granting rights to groups;
Managers for inviting employees into groups.
Access typeReading partial or full data depending on rights;
Launching the data-creation process (during a transfer request or account creation);
Launching the data-modification process (PIN modification request).

DOMINO SAS server:

Application roleMachines on which processes execute.
UsersNA
Authorization-determination ownerIS administrators.
Access managerIS administrators.
Access typeCompartmentalized access to sensitive payment data;
Outside the sensitive payment data management module, the rest of the servers have access only to tokens or truncated data.

Partners:

Application rolePartner servers (Mastercard, Arkéa and Exceet).
UsersNA
Authorization-determination ownerNA
Access managerIS administrators.
Access typeLimited access to data managed by the partners themselves;
No access to data present in DOMINO SAS databases.

Description of the IS dedicated to payment-service activities and the technical security measures implemented

Customer applications can read data only because they are connected to DOMINO SAS APIs.

These APIs in turn call modules (including the sensitive payment data management module). Modules communicate with one another inside a VPC (Virtual Private Cloud: a private cloud isolated from the rest of the DOMINO SAS infrastructure and located in a public cloud).

Implementing a VPC reduces the need to secure or encrypt exchanges between modules. However, each module exposes its data and methods through an internal API describing the rights that other modules have to read / write data under that module’s responsibility.

Technical security measures implemented by DOMINO SAS:

API / application connection:

API / partner connection:

Exposure of APIs and databases to the internet:

Separation of the sensitive payment data management module:

Description of how security breaches will be detected and handled

DOMINO SAS benefits from the system implemented by AWS: AWS provides dashboards listing all identified attack attempts and provides access to statistics for each of them by site.

The list of blacklisted malicious IP addresses is accessible to authorized persons within the DOMINO SAS IT technical team.

An alert system is connected to the monitoring system to notify administrators in the event of anomalies in system requests.

Annual internal control program relating to IT systems security

DOMINO SAS uses specialized security companies annually to test the security and reliability of its information systems. Penetration tests will therefore be performed in three different forms:

The scope of these tests covers:

Because the company responsible for DOMINO SAS’s security audit changes each year, it is not currently possible to provide the name of the company responsible for the next internal control of DOMINO SAS information systems security.

Business continuity mechanism and security policy

Business continuity

Identified risks

DOMINO SAS conducted an impact study that led to identification of the following elements that could jeopardize business continuity:

Possible disruptions and associated risks:

Causes of disruptions (with the related resulting disruption numbers):

These risks are detailed in the Emergency and Business Continuity Plan (Plan d’Urgence et de Poursuite d’Activité, PUPA) included as an annex (7.3.6 Emergency and Business Continuity Plan).

Most of these risks can be managed and contained in less than a few days and, for a large portion of them, in less than 24 hours.

Nevertheless, whatever the risk in question, DOMINO SAS considers that data loss associated with occurrence of the risk cannot exceed 24 hours.

Backup site

DOMINO SAS does not have a backup site. If the main site is unavailable, employees are asked to work from home or from a coworking space of their choice.

Emergency plan and remediation procedures

The means implemented by DOMINO SAS to ensure business continuity in cases of service interruption linked to the risks identified in section 11.1 are detailed in the PUPA (7.3.6 Emergency and Business Continuity Plan).

In summary:

PUPA test

The PUPA is tested annually by the person responsible for triggering the PUPA (the RSSI) to ensure its relevance and effectiveness. The objectives of the tests are to verify:

This annual test results in a report analyzing the level of performance of the plan during simulations and accompanied by recommendations to improve it. It is important that these tests be as realistic as possible to prepare the organization effectively to manage a real crisis.

The controls performed during the tests are as follows:

The test report contains the following information:

Payment services security policy

Description of the information systems

Technical architecture diagrams for systems and networks

Architecture diagram - summary view (the focuses are in the following diagrams)

Converted visual summary of image33.png:

1 - focus - Internal Developer Platform

Converted visual summary of image41.png:

This set is fully managed by third-party partners orbiting around GitHub (the code repository).

2 - focus - Technical and security sections of the VPC

Converted visual summary of image67.png:

These building blocks allow DOMINO SAS teams to manage:

They do not directly process information and do not apply any business rule, but they are necessary for proper system operation.

3 - focus - Internal modules

Converted visual summary of image45.png:

The internal modules correspond to the building blocks that will be able to execute DOMINO SAS procedures and apply DOMINO SAS business rules.

This is the core of the DOMINO SAS Information System.

These modules are deployed according to the following logic:

Converted visual summary of image29.png:

The procedures for connecting the IS to partners and those relating to sensitive payment data are described in section 10.3.6 “Description of the IS dedicated to payment-service activities and the technical security measures implemented”.

Development of the executable for internal modules

For all internal modules making up its IS, DOMINO SAS has chosen the “replicated monolith” approach. A compiled executable (programmed in Rust) is deployed in EKS and replicated across 3 availability zones (= 3 data centers), as described in section 11.2.1.1 “Technical architecture diagrams for systems and networks”.

The monolith is developed internally in an agile framework. Business and IT teams coordinate to write technical specifications, which are then implemented, tested and continuously improved.

The entire process and deliverables for building, maintaining and hosting internal modules will be PCI DSS certified.

Distinction between the IS supporting operational activities and the support IS for organization and administration of the activity

The IS supporting operational activities is totally distinct from the support IS for organization and administration of the activity.

The first is composed of a major building block developed internally and integrations with the various partners and service providers.

The second is composed of a cluster of external SaaS services that cannot interact with the first.

External connection management

Access to the IS is controlled through authorizations and unauthorized access to the IS is impossible. DOMINO SAS has implemented formal procedures to control the granting of IS access rights:

DOMINO SAS authorizes remote access to its IS only to specific service providers / partners within a scope defined according to the procedures defined by DOMINO SAS, equivalent to those applicable to employees. To the extent possible, access is opened only for the duration of the intervention, for example during maintenance operations. However, remote control of a DOMINO SAS workstation is prohibited.

Internal connection management

A comprehensive IS security framework has been implemented to restrict access to operating systems only to authorized users, in accordance with the rules defined for profile allocation, as described in the IS Security Policy.

In summary:

Converted visual summary of image18.png:

Segregation of the customer environment

DOMINO SAS infrastructure and systems will be hosted in 3 data centers located in the 3 availability zones of the selected AWS region (in Continental Europe).

This region remains to be defined and will depend on the AWS region chosen by Mastercard for implementation of the CloudEdge infrastructure to which DOMINO SAS will be connected. The 3 availability zones will be redundant and will have exactly the same function. If one of them fails, requests will be redirected to the other two. The entire infrastructure is behind an AWS load balancer that will route requests to the first available instance. Data will be managed by AWS with synchronous and asynchronous replication systems (depending on the database and its use) between the 3 sites. In the event of simultaneous loss of the three sites, backups will be saved by AWS in a different region.

DOMINO SAS will have 1 production environment per site, on which services intended for customers will run. Additional environments may be started on the fly or kept permanently for internal tests and platform-development tooling. These environments will be isolated through VPCs (virtual private computer), ensuring strict partitioning of each environment. DOMINO SAS will have in particular the following environments:

Physical security mechanisms and measures for premises, equipment and IT systems

Physical security measures and mechanisms for premises, equipment and IT systems are described in the IS Security Policy.

In summary:

DOMINO SAS employees are equipped with laptops, not connected in a network, made available to them by DOMINO SAS. They are directly connected to the internet and access company resources via a VPN. The entire IT fleet is treated as if it were permanently mobile. This unique operating mode simplifies processing and makes the usual exception the norm.

DOMINO SAS uses Primo as an MDM solution for workstations, thereby allowing DOMINO SAS to administer computers and ensure their proper use / erasure in the event of a problem.

DOMINO SAS rents premises and is not directly responsible for their operation. DOMINO SAS nevertheless ensures that it rents offices that have the following elements:

DOMINO SAS prohibits printing.

Because DOMINO SAS is cloud native, there is no file that is not backed up online. Loss, theft or breakage of equipment therefore has no consequence for the data being worked on.

Payment services process security policy

Logical diagrams of operation flows

Onboarding
1. Common core for DOMINO SAS activities as Payment Institution and agent of PPS EU SA
  1. The user goes to the application;

  2. The user starts onboarding by clicking the create-account button;

  3. Onboarding data will be sent progressively to DOMINO SAS servers over HTTPS (TLS 1.2);

  4. First, the user simply sends their email and a unique device ID is generated client-side. This ID will be stored by the user in the secure storage of the user’s smartphone;

  5. On receipt of the email and unique ID, an account is created and the first session contains the unique device ID and the other login information. The data are stored in a managed MongoDB database and the fields are encrypted at rest;

  6. The server sends a request (TLS 1.2) to customer.io to issue and send a 6-digit validation code to the user by email (this is email verification);

  7. The user fills in the user’s information and signs the contract in several steps, sending them to the server (personal information and contact details);

  8. When verifying the user’s identity, the user continues the experience through the ubble.ai platform, integrated as an iframe in the application when the smartphone is compatible. If not, the user is redirected to this platform in the smartphone browser. This process does not pass through the DOMINO SAS IS;

  9. The user makes the initial funding of the payment account with the user’s personal bank card in the application. This funding is performed through a payment gateway operated by Stripe. This process does not pass through the DOMINO SAS IS;

  10. Asynchronously, Stripe sends a payment notification to a webhook opened on the DOMINO SAS servers dedicated to this partner (over HTTPS, on port 443);

  11. Asynchronously, Ubble sends a notification of information completion to a webhook opened on the DOMINO SAS servers dedicated to this partner (over HTTPS, on port 443);

  12. DOMINO SAS employees check onboarding statuses daily and, when the data are complete, they can validate account creation in the back office (access via VPN);

  13. This step depends on the setup; see sections 11.3.1.1.2 and 11.3.1.1.3 below;

  14. On receipt of the payment card by post, the customer can activate the card, which validates the account and address definitively.

2. Specific features of the activity as agent of PPS EU SA
  1. After validation of a customer by a DOMINO SAS employee, DOMINO SAS servers communicate the customer’s identity and contact information to PPS EU servers to request creation of the payment account;

  2. Outside the DOMINO SAS perimeter, PPS EU creates the customer’s payment account in its IS and communicates with Mastercard and Exceet to create the payment card;

  3. The daily volume of cards issued by Exceet is sent by email to DOMINO SAS employees subscribed to the corresponding distribution list (Executive Committee members, Customer Service and Compliance).

3. Specific features of the activity as Payment Institution
  1. The DOMINO SAS Core Banking module creates an available payment account and IBAN and assigns them to the customer. DOMINO SAS ledger and account-book databases are updated;

  2. The received top-up is credited to the customer’s account;

  3. A PAN, PIN, expiration date, token and cardholder name are initialized for the card in the “cards issuing and management” module. The card order file is created and transferred to Exceet;

  4. Mastercard is notified of the card creation.

Converted visual summary of image61.png:

Adding a beneficiary
4. PPS EU SAS agent activity

From the application, the customer requests addition of a beneficiary. On receipt, the DOMINO SAS server transmits the request to PPS EU, which adds a beneficiary to the customer’s payment account.

5. Payment Institution activity

From the application, the customer requests addition of a beneficiary. On receipt, the DOMINO SAS server adds it to the corresponding database.

Converted visual summary of image4.png:

Making an outgoing transfer
6. PPS EU SA agent activity

From the application, the customer requests issuance of a transfer. On receipt of the request, the DOMINO SAS server transmits it to PPS EU SAS, which operates the transfer. Connection to PPS EU SA is by HTTPS, encrypted in TLS 1.2.

Balances are updated when DOMINO SAS receives a notification from PPS indicating that the transfer has been performed.

7. Payment Institution activity

From the application, the customer requests issuance of a transfer. On receipt of the request, the DOMINO SAS server adds it to the list of transfers to be processed in the corresponding database.

Balances are updated “optimistically”, meaning that they are updated before it is known whether the operation has worked, to avoid double spending or exceeding limits in the event of multiple rapid transactions.

At the time of daily processing, the transfer is sent to Arkéa for transmission to the CSM. Connection to Arkéa is over HTTPS, encrypted in mTLS for the EBICS part and in SFTP (SSH File Transfer Protocol). Key exchanges have taken place beforehand.

Converted visual summary of image66.png:

Making a card payment
8. PPS EU SA agent activity

DOMINO SAS receives a notification from PPS EU SA that a transaction has been processed by Mastercard and PPS EU SA on the dedicated webhook.

The DOMINO SAS server sends a notification to the customer via customer.io, which will transmit a notification to the customer’s mobile phone.

9. Payment Institution activity

The authorization request comes from Mastercard servers. It reaches the DOMINO SAS VPC through the MIP managed by Mastercard in the same AWS region as the VPC.

The response goes to Mastercard over the internet in mTLS.

Communication with Arkéa for release from safeguarding/ring-fencing (décantonnement) and settlement of funds is over TLS and SFTP.

Converted visual summary of image21.png:

Making an ATM withdrawal

An ATM withdrawal works in the same way as a card payment from the network and security standpoint:

Technical characteristics of payment instruments

Customer device authentication procedure

This procedure allows storage of a signing key on the user’s device, which will then allow the user to send signed requests to perform all necessary operations.

This storage is permanent for the mobile application because it is performed in secure storage. It is temporary (for the duration of a brief user session) for the browser.

Initiation phase:

  1. The customer requests authentication on the mobile application;

  2. The authentication service verifies that the customer exists in the database;

  3. The authentication server sends an authentication code to the customer’s email address;

  4. The end customer reads the OTP (One Time Password) in the email;

  5. The customer sends the OTP and the last 4 digits of the physical card, and the application adds a self-generated signature that will be stored in mobile secure storage;

  6. The authentication servers store the session token and signature. This step triggers deletion of any previous session for this user;

  7. The data are propagated for replication in all replicas;

  8. Login data are analyzed in real time and scored to verify that no suspicious behavior is present. Consecutive attempts, geographic movement, use of a VPN and other elements to be determined make it possible to assess the legitimate nature of the login;

  9. An email notification is sent to the customer. If the customer did not initiate this login, the customer must contact support urgently.

Operating phase:

  1. The customer’s mobile application makes a request with a session token and an HMAC (https://fr.wikipedia.org/wiki/HMAC) generated with the stored signature, a nonce and a timestamp. The signature is never exchanged after the initiation phase;

  2. The authentication server retrieves the user and compares the session token and the expected HMAC;

  3. The authentication server returns a 401 if authentication fails;

  4. If authentication succeeds, the authentication server forwards the request to the target service.

Recovery process:

If a customer is locked out of the application because the customer has lost the physical card and cannot find the last digits of the PAN, DOMINO SAS orders a new card for the customer so the customer can access the application again. In the meantime, DOMINO SAS Customer Service may act on behalf of the customer by blocking the card or giving the customer basic information about the payment account (balances, transactions, etc.).

If a customer is locked out of the application because the customer has lost access to the email, Customer Service must ensure that this is the right person. It may then update the customer’s email address, after which the customer can log in with the updated address.

Expected benefits of the choices made by DOMINO SAS - absence of password:

Converted visual summary of image74.png:

List of main formalized procedures relating to IT systems

The main formalized procedures relating to DOMINO SAS information systems are as follows:

These procedures are included as annexes.

Project implementation schedule

Projected schedule:

Converted visual summary of image28.png:

Date / periodMilestone
Oct. 2023Meeting to present the project to the ACPR
Jan. 2024Filing of the dossier
June 2024Authorization subject to conditions precedent (Agrément sous conditions suspensives)
Sept. 2024Final authorization (Agrément définitif)
Q4 2024CBS operational and integrated with partners
Q1 2025Restricted B2B and B2C “Friends and Family” launch under Green-Got authorization
End Q1 2025Commercial B2B launch under Green-Got authorization
Q2 2025B2C launch under Green-Got authorization
Q3 2025B2C switch from PPS EU to Green-Got

DOMINO SAS envisages obtaining its authorization subject to conditions precedent at the end of Q2 2024 / beginning of Q3 2024.

Switch-over schedule

EPS accounts are closed and transfer redirection is implemented to the new IBANs for a period of 3 months.

Customers accept the new DOMINO SAS GTUs.

3-year project schedule

Before obtaining authorization:

DOMINO SAS will finalize technical developments concerning:

DOMINO SAS will sign the following contracts:

Upon obtaining authorization:

Source Footnotes

1

https://paleblue.vc/

2

https://www.ipsos.com/sites/default/files/ct/news/documents/2023-10/Ipsos-Cese-Etat-de-la-France-septembre-2023%201.pdf

3

https://www.ipsos.com/sites/default/files/ct/news/documents/2023-10/Ipsos-Cese-Etat-de-la-France-septembre-2023%201.pdf

4

https://www.reuters.com/sustainability/climate-energy/eu-needs-over-760-blnyr-hit-green-transition-targets-commission-2023-07-06/#:~:text=%22Overall%2C%20additional%20investments%20of%20about,said%20in%20a%20statement%20on

5

https://www.economie.gouv.fr/facileco/principaux-chiffres-france-et-europe#

6

https://www.moneyvox.fr/banque-en-ligne/actualites/90917/banque-pro-en-ligne-combien-de-clients-ont-saute-le-pas-en-france#:~:text=Fin%20octobre%202021%2C%20l’agence,%C3%A0%20nouveau%20augmenter%20en%202022.

7

https://www.statista.com/statistics/944142/banking-population-in-europe-by-country/

8

https://www.grandviewresearch.com/industry-analysis/neobanking-market

9

https://www.statista.com/statistics/971163/customers-of-selected-european-neobanks-and-challenger-banks/

10

https://www.reuters.com/business/finance/monzo-valuation-jumps-45-billion-after-fresh-funding-round-2021-12-08/

11

https://www.ctvc.co/climate-tech-h1-2023-venture-funding/

12

https://tech.eu/2023/06/29/europe-dominates-in-climate-fintech-funding-despite-overall-funding-slump-in-2022/

13

https://www.bankingonclimatechaos.org/

14

https://green-got.com/articles/selection-assurance-vie

15

https://www.lerevenu.com/placements/assurance-vie/lassurance-vie-concerne-surtout-les-seniors

16

https://www.boursorama.com/patrimoine/fiches-pratiques/crowdfunding-immobilier-faut-il-investir-88b71eb61700a9402467d2536f1dc1cc#:~:text=%2D%20Le%20crowdfunding%20est%20un%20placement%20accessible&text=L’investissement%20moyen%20%C3%A9tait%20de,2020%20%C3%A0%20313%20en%202021*.

17

https://www.boursorama.com/patrimoine/actualites/epargne-combien-les-francais-placent-ils-en-moyenne-sur-leur-livret-a-b46bf8cab3c17b9b3a6b41eaaf568e93

Data retention basis

French Banking Data Retention Basis

This document summarizes the regulatory basis used to determine retention periods for customer information handled by Green-Got as a French banking and payment services organization.

Summary

Customer and transaction information is retained only where required for an active business purpose, a regulatory obligation, accounting or tax obligations, dispute handling, fraud prevention, or legal hold.

After account closure or the end of the customer relationship, data that must be retained is removed from ordinary operational use where feasible and kept only for the applicable retention purpose. Data that is no longer required is deleted or anonymized.

Regulatory Retention Periods

Data categoryRetention basisRetention period
Customer relationship and KYC recordsFrench AML/CFT obligations under Code monetaire et financier Article L561-125 years after account closure or end of the customer relationship
Customer operation and payment recordsFrench AML/CFT obligations under Code monetaire et financier Article L561-125 years after execution of the operation
Closed card records and required cardholder dataLegal, regulatory, accounting, tax, dispute, audit, or compliance purpose applicable to the closed account or related operations5-10 years after account closure, depending on the applicable retention duty
Payment dispute support dataPayment service dispute period under Code monetaire et financier Article L133-24At least 13 months after debit where needed to handle unauthorized or incorrectly executed payment claims
Tax audit supporting documentsLivre des procedures fiscales Article L102 B6 years from the last operation recorded or from document creation, depending on the record
Accounting documents and supporting evidenceCode de commerce Article L123-2210 years

Application To Green-Got Data

The Data Management Policy retention matrix uses a 5-10 year range for customer, closed-card, and payment-related records because multiple French retention duties may apply to the same record:

Where multiple retention duties apply to the same record, Green-Got applies the longest applicable legal or regulatory retention period. Where no legal or regulatory retention duty applies, Green-Got retains personal data only for the period needed for the documented processing purpose.

For closed cards, retained records identify the card, lifecycle events, deactivation status, related account, related transactions, and supporting evidence required for the applicable retention purpose. The 5-10 year range does not require every original card credential value to remain recoverable for the full retention period.

Card Data And CVV/CVC

Green-Got distinguishes closed card records, issuer-side card credential records, and card verification data received in authorization flows.

When a customer closes their account, related cards are permanently deactivated and the CVV/CVC value is no longer usable for payment authorization. Deactivation eliminates payment fraud risk for the closed card credential, but it does not create an independent retention purpose under data protection rules.

Issuer-side card credential values are retained only while required for issuing operations, cardholder service functionality, regulatory obligations, dispute handling, or legal hold. After card closure, CVV/CVC values are deleted or rendered unrecoverable unless a documented legal, regulatory, dispute, or legal-hold requirement applies to that specific value.

Card verification data received during authorization is processed only for authorization and is not retained as a transaction artifact.

Sources

PCI DSS

A process is in place to determine whether certain OS types do or do not require malware protection

Provide evidence that periodic evaluations are performed to determine if evolving threats apply to systems not typically impacted by malware. Guidance: Systems not commonly impacted by malware (e.g., certain Linux builds) may not require the use of anti-malware software (e.g., Falcon Crowdstrike). If your organization has made a risk-based decision to forego the use of malware protection on these systems, update your annual Risk Assessment to reflect this, and provide evidence of a process (e.g., screenshots of subscriptions to vendor and security alerts, zero-day notifications, threat intelligence briefings per Requirement 6.1.) to ensure that evolving malware and attacker threats are monitored.

Evidence

Green-Got evaluates malware exposure by platform type as part of the PCI DSS security program and vulnerability management process. The current platform assessment is:

Platform typeMalware protection decisionCurrent controls
Windows workstationsAnti-malware requiredMicrosoft Defender, configured and enforced through Primo MDM
macOS workstationsAnti-malware requiredApple XProtect, built into macOS and not user-disableable; related endpoint controls monitored through Fleet
Linux workstationsExcluded from the workstation malware scanner scopeLinux work machines remain covered by endpoint security controls, patching, and employee-workstation risk review
Amazon Linux EC2 hosts used for ECSAWS-native malware protection enabledEC2 hosts are rotated; Amazon Inspector provides vulnerability scanning; GuardDuty Runtime Monitoring provides runtime threat detection; GuardDuty Malware Protection for EC2 scans EBS volumes attached to EC2 instances and ECS-on-EC2 workloads
Application containersCovered through server-side AWS-native and deployment controlsContainer filesystems are read-only at runtime; base images and application dependencies are scanned before deployment; runtime activity is covered by GuardDuty Runtime Monitoring on the EC2 hosts

The submitted Malware configuration and Malware protections deployed evidence documents the current controls for each platform. Windows and macOS workstations receive anti-malware controls directly. Linux workstations are excluded from the workstation malware scanner scope through this assessment. Server-side Linux hosts and containers are evaluated differently because containers run with read-only filesystems and EC2 hosts are rotated. Green-Got uses vulnerability scanning of container images, EC2 hosts, and application dependencies to verify that malware-relevant vulnerabilities are not introduced through the base image or dependency chain.

Green-Got distinguishes vulnerability scanning, runtime threat detection, and malware scanning for server-side systems. Amazon Inspector is the vulnerability scanning control for EC2 hosts and container images. GuardDuty Runtime Monitoring is the runtime threat detection control for EC2-hosted ECS workloads. GuardDuty Malware Protection for EC2 is the AWS-native malware scanning control for EC2 and ECS-on-EC2 workloads, scanning EBS volumes attached to EC2 instances and container workloads running on EC2.

Periodic evaluation uses the documented security review inputs below:

Green-Got reassesses whether additional anti-malware controls are required when vulnerability intelligence, vendor advisories, Amazon Inspector findings, GuardDuty findings, or risk assessment updates indicate that a platform previously treated as lower malware risk is exposed to evolving malware threats.

Access and IAM Credentials Rotated ⏱️

Evidence

We rely on short-lived AWS credentials rather than long-lived access keys, which reduces or eliminates the need for manual key rotation.

Access list for production application(s)

Screenshot or export of list of users with access to in scope production applications not integrated with Vanta and permissions/roles associated with those users

Evidence

This is not applicable to us. All users with access to in scope production applications are integrated with Vanta.

Access request ticket and history ❌

Provide two examples of a recent access request and approval (ticket, email etc) to grant employee access to a critical or privileged system. An example of a system would be any system vital to the functioning of internal and external business, such as your HRIS (e.g., BambooHR), your infrastructure cloud platform (e.g., AWS), or CI/CD platform (e.g., Jenkins) and requires an additional level of role-based access control to operate various functionality within the system itself (e.g., regular user, editor, admin, super admin). Guidance: The best practice is to track and document approvals through tickets. However, if tickets aren’t being utilized, email threads and/or a manual access request form may also be acceptable. Here’s a sample access request form: Google docs template / Docx template.

Access requests denied log

Provide evidence (e.g., application logs) from a recent access denied request (failed authentication, authorization, or failed resource access) to your application by an external user.

Evidence

We are collecting logs, traces and metrics about access denied request (failed authentication, authorization, or failed resource access). We keep these logs for 90 days.

Trace list

Trace

Metric

Access to cardholder data and cardholder data environment require explicit management approval ❌

Evidence that shows approval granted for those with access to the CDE and CHD. Guidance: Provide screenshot or sample tickets showing approval workflows and management signoff.

Evidence

Account lock on failed logins ❌

Provide screenshots or other evidence that after a defined number of failed log-in attempts, the account in question is locked and cannot be authenticated until the hold is removed by another member of the organization (e.g., IT or Security manager).

All encryption processes are fully documented ❌

Evidence that encryption processes are documented. Guidance: Document key management processes that includes how keys are:

All other system logging covered ❌

Provide logs or log configurations from any other systems not already identified and accounted for.

Anti malware logs collected stored

Provide an example of Antimalware logs present in your environment. Ideally these logs should be feeding into your overall log aggregation tooling for correlation with other events. At absolute minimum they must be retained in accordance with your organizations defined log retention policy.

Evidence

Green-Got uses AWS GuardDuty with Runtime Monitoring and Malware Protection for EC2 enabled on EC2 instances hosting ECS workloads. The GuardDuty agent is automatically deployed via AWS Systems Manager (SSM) for Runtime Monitoring. GuardDuty Malware Protection for EC2 provides agentless malware scanning of EBS volumes attached to EC2 instances and ECS-on-EC2 workloads.

GuardDuty generates findings for suspicious or potentially malicious behavior, which serve as security event logs. Malware Protection for EC2 also records scan status and scan result events, including clean, infected, skipped, and failed scan outcomes. Findings are published every fifteen minutes and routed via Amazon EventBridge to an SNS topic, which delivers email alerts to the security team. GuardDuty findings and malware scan results are retained within GuardDuty for 90 days.

Server-side malware risk is addressed through hardened infrastructure, restricted access, continuous runtime threat detection, and GuardDuty Malware Protection for EC2.

Findings

Antimalware tamper protections ❌

Provide screenshots of the result of attempts to disable endpoint malware. End users should not be able to pause or stop protection without administrative access and documented approval.

Evidence

Application - PAN protection ⏱️

Provide a screenshot showing that the PAN in Applications is encrypted or otherwise protected.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-04-30
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material PAN protection change

Green-Got protects PAN in application flows through encryption at storage, full obfuscation by default, and masked display only in authorized display contexts.

Stored PAN is rendered unreadable through application-side envelope encryption backed by AWS KMS. The database stores encrypted PAN envelopes rather than clear PAN values. This is documented with storage evidence in PAN is rendered unreadable anywhere it is stored and PAN Encryption or Obfuscation - Database.

Encrypted PAN database value

Application and support views do not display full PAN by default. PAN is fully obfuscated in default application flows. Standard application and support display contexts show only masked PAN, as documented in Primary Account Number (PAN) is masked when displayed. Full PAN display is handled separately through a restricted cardholder-requested reveal flow after step-up authentication, outside the standard masked display contexts.

Masked PAN display

PAN lookup does not require decrypting stored PAN. Green-Got uses a keyed HMAC-SHA256 lookup value for deterministic matching. The application computes the keyed HMAC lookup value locally with PAN lookup HMAC keys loaded at startup from protected AWS Systems Manager Parameter Store SecureString values, and the controls preventing correlation of retained PAN representations are documented in Prevention of PAN Reconstruction.

Change History

VersionDateAuthorChanges
1.02026-04-23julian@green-got.comDocument PAN protection controls in application flows.
1.12026-04-30julian@green-got.comAdd document control metadata and align PAN lookup HMAC wording with startup-loaded AWS Systems Manager Parameter Store secrets.

Application session timeout

Screenshot of code that defines the timeout period for terminating a session within your application for the end user. Guidance: This is relevant for companies that have a web application, mobile application, or API where an end consumer must authenticate into the system.

Evidence

This evidence applies to the customer-facing authentication service used by end users on web and mobile clients. Authenticated user sessions expire after 5 minutes of inactivity. The authentication flow sets the session expiry to 5 minutes at login and refreshes that same 5-minute inactivity window on each valid request.

pub const SESSION_DURATION: Duration = Duration::minutes(5);

let session = Session {
    id: SessionId::new(),
    user_id: user_id.clone(),
    device_id: device.id.clone(),
    device_type: device.device_type,
    status: SessionStatus::Valid,
    created_at: Utc::now(),
    expires_at: Utc::now() + SESSION_DURATION,
    credential_kinds: vec![credential_kind],
};

// Extend expiry on every valid request (5 min inactivity timeout)
let new_expires_at = Utc::now() + SESSION_DURATION;
session_store::extend_expiry(&mut **tx, session_id, new_expires_at)
    .await
    .map_err(ValidateSessionError::Internal)?;

Asset inventory

Master asset inventory list for in-scope systems that includes all systems connected to the network and the network devices themselves (e.g. routers, switches, firewalls, VOIP telephones, printers, removable devices (USBs, CDs, etc.), laptops, desktops, digital media, backup media, SANs, physical and virtual servers, databases, approved BYOD devices, authorized WAP, etc.) *Will be used by the assessor for sampling of device configuration/hardening reviews

Evidence

All in-scope systems and network devices are tracked in Vanta Inventory.

Authenticated internal scan configuration ❌

Provide documentation that shows internal vulnerability scans are configured to run with authentication (i.e., credentialed scans) to ensure comprehensive visibility into system vulnerabilities. This includes evidence that valid credentials are used during scanning and that scans are capable of identifying configuration-level and patch-related vulnerabilities not visible to unauthenticated scans.

Authentication Factor Revocation or Reassignment ❌

Provide documentation or system evidence demonstrating the revocation or deactivation of authentication factors when no longer needed or upon user offboarding.

@chloe

Authentication data storage transmission and protection

Provide evidence that authentication factors (e.g., passwords, OTPs, certificates, tokens, or smart card data) are protected using strong cryptographic methods whenever transmitted over networks. This includes between client and server, between internal services, or between integrated platforms. Upload one or more of the following:

Vendor documentation or configuration screenshots confirming that secure transmission methods (e.g., TLS 1.2+, SSH, IPsec) are enforced for authentication traffic. A sample transmission capture or debug log (e.g., from Wireshark or browser dev tools) showing authentication data is encrypted in transit and not visible in plaintext. Screenshots of protocol settings in identity providers, application servers, or APIs demonstrating the use of HTTPS, encrypted tunnels, or mutual TLS (mTLS) for authentication-related communications. For API-based authentication: provide headers or payload samples showing tokens or secrets are only transmitted over secure channels. Note: Do not include any sensitive credentials in evidence.

Evidence

Everything we send over the network is done via encrypted connections this is not limited to authentication factors.

Internal communication outside of our VPC is done via Tailscale exclusively communicates over WireGuard which is one of the most secure VPN technology available making any protoll used inside of inherently secure.

Communication inside the VPC is inherently private and tightly controlled via security groups. For reliability reasons our server communicates with our load balancer via http but since this communication happens inside our VPC we see it as unnecessary to change this to https.

Here is a detailed diagram of all the protocols used and where Protocols

Our infrastructure as code enforces redirect from http to https and a minimum TLS version of 1.2.

new aws.cloudfront.Distribution("cloudfront", {
  distributionConfig: {
    httpVersion: "http2and3",
    viewerCertificate: {
      sslSupportMethod: "sni-only",
      minimumProtocolVersion: "TLSv1.3_2025",
    },
    defaultCacheBehavior: {
      viewerProtocolPolicy: "redirect-to-https",
    },
  },
})

new aws.globalaccelerator.Listener("tcpListener", {
  acceleratorArn: accelerator.arn,
  protocol: "TCP",
  portRanges: [{ fromPort: 443, toPort: 443 }],
})

// for http3
// TLS version cannot be configured is enforced by AWS to be TLSv1.2 at a minimum https://docs.aws.amazon.com/global-accelerator/latest/dg/infrastructure-security.html
new aws.globalaccelerator.Listener("udpListener", {
  acceleratorArn: accelerator.arn,
  protocol: "UDP",
  portRanges: [{ fromPort: 443, toPort: 443 }],
})

can also be verified using curl

curl http://green-got.co
  <html>
  <head><title>301 Moved Permanently</title></head>
  <body>
  <center><h1>301 Moved Permanently</h1></center>
  <hr><center>CloudFront</center>
  </body>
  </html>
curl --tlsv1.1 --tls-max 1.1 https://green-got.co
  curl: (35) TLS connect error: error:00000000:lib(0)::reason(0)

Authentication fails closed ❌

Evidence that authentication mechanisms used will default deny in the event of failure or attacks such as brute force, password spraying, etc. (Vendor documentation, configuration information from IDP or authentication systems, etc)

@chloe

Backup media is stored securely

Evidence that backups on external media are protected from compromise via controls such as encryption, physical security, etc. while in storage (if applicable) Note: NA if not backup media is used

Evidence

We dont use any backup media, backup’s. Backup’s are managed by AWS, all data is encrypted at rest. AWS handled physical security.

Backup media is transported securely

Evidence that external backup media is encrypted or otherwise protected via physical security controls when outside of secure areas for transportation (if applicable) Note: NA if not backup media is used

#Evidence

We don’t use any backup media. Backups are managed by AWS, all data is encrypted at rest and in transport. AWS handled physical security. We don’t move backup’s physically.

CVSS score risk ranking process evidence request ❌

What you need to upload: Provide documentation or records demonstrating the organization’s process for assigning risk rankings to security vulnerabilities, specifically showing how the CVSS score is considered in this process. Include any relevant policies, procedures, or reports that outline the roles and responsibilities in technical vulnerability management. Where can you find this information: This information can typically be found in the organization’s IT security policy documents, vulnerability management procedures, or risk assessment reports. It may also be available in internal audit reports or compliance documentation related to security practices.

Cardholder dataflow diagram has been created

Provide a document showing all cardholder dataflows across your systems and networks. Guidance: Create a diagram showing all cardholder data flows within your environment. See this example cardholder dataflow diagram: Google docs template / Docx template Best Practices: The diagram should include all ingress/egress connection points for account data, including any connections to open, public networks, application processing flows, storage, transmission between systems and networks, and any file backups. Note: The data-flow diagram is meant to be complementary to your network diagram and should augment it. You can consider including the following in your data-flow diagram:

  1. All processing flows of account data, including authorization, capture, settlement, chargeback, and refunds.
  2. All distinct acceptance channels, including card-present, card-not-present, and ecommerce.
  3. All types of data receipt or transmission, including any involving hard copy/paper media.
  4. The flow of account data from the point where it enters the environment, to its final disposition.
  5. Where account data is transmitted and processed, where it is stored, and whether storage is short term or long term.
  6. The source of all account data received (for example, customers, third party, etc.), and any entities with which account data is shared.
  7. Date of last update, and names of people that made and approved the updates.

Cardholder Data Flow Diagram

Document Control

FieldValue
Document Status🔄 In Progress (Architecture designed, implementation ongoing)
Current Version0.2
Last Updated2026-04-21
Document OwnerCore Banking Team
Approved By[Pending - CISO/QSA Approval Required]
Next Review Date2026-07-02

Version History

VersionDateAuthorApproverChangesStatus
0.22026-04-21Core Banking TeamPendingAdd 3DS AAV authorization flow and apata_correlation_id documentationIn Progress
0.12026-04-02Core Banking TeamPendingInitial draft - Architecture and design documentationIn Progress

Note: This document requires formal approval from CISO and/or QSA before being considered complete for PCI DSS compliance purposes.


Overview

This document provides evidence of PCI DSS compliance by documenting all cardholder data (CHD) flows across Green-Got’s Cardholder Data Environment (CDE).

Comprehensive Technical Documentation:
📄 3_cardholder_data_flow.md - Complete data flow diagram with all acceptance channels


Related Technical Documentation

Core Banking Documentation

Compliance Documentation


Notes

Implementation Status: Architecture and design complete, implementation in progress. This documentation will be finalized and formally attested once implementation is complete and verified.


For detailed technical information, see: 3_cardholder_data_flow.md

Cardholder dataflow diagram process ⏱️

Provide a document of a process that ensures your cardholder dataflow diagram is kept up to date. Guidance: Update the diagram whenever significant changes are made to the CDE architecture, and check that your firewall configuration standards align to what is depicted in the diagram. Include revision history and dates in diagram along with the names of people that made and approved the updates.

Process

Update Triggers

The cardholder data flow diagram is updated when any of the following occur:

Update Procedure

  1. The Core Banking Team identifies the change and its impact on cardholder data flows.
  2. The diagram in 3_cardholder_data_flow.md is updated to reflect the change.
  3. The Document Control section in cardholder_dataflow_diagram_exists.md is updated:
    • Version number is incremented
    • Date, Author, and description of changes are recorded in the Version History table
    • Document Status is set to “In Review”
  4. The updated diagram is submitted to the CISO for approval.
  5. Upon approval, the “Approved by” field is filled in and Document Status is set to “Approved”.

Firewall Configuration Alignment

When the diagram is updated, the Core Banking Team verifies that the firewall configuration standards documented in network_diagram/index.md align with the updated diagram. Any discrepancies are resolved before the diagram is submitted for approval.

Revision History

All changes to the cardholder data flow diagram are tracked in the Version History table in cardholder_dataflow_diagram_exists.md, recording:

This provides an auditable trail of all modifications to the diagram, satisfying the requirements of PCI DSS 12.5.2 and 12.5.3.

Cardholder network diagram process ⏱️

Provide a document describing the process used to keep the cardholder network diagram accurate and up to date.

Evidence

Green-Got maintains the CDE network representation in the Network diagram and the cardholder data flow details in 3_cardholder_data_flow.md. These documents are complementary: the network diagram describes CDE boundaries, ingress paths, internal connectivity, administrative access, and network security controls, while the cardholder data flow document describes where account data is received, processed, transmitted, and stored.

Green-Got operates a backend monolith rather than a separately deployed cardholder network. Cardholder functions are logically separated for auditability inside the backend ecosystem, and the full backend environment is treated at the same security level as the cardholder data environment. This approach avoids relying on internal segmentation claims for the cardholder code path and keeps the effective PCI DSS scope aligned with the systems that store, process, transmit, or affect the security of cardholder data.

Green-Got operates as a card issuer providing banking and card services directly to its own end customers. For PCI DSS assessment purposes, Green-Got is treated as a service provider because its issuing and card-processing environment stores, processes, transmits, and affects the security of cardholder data and sensitive authentication data as part of the payment ecosystem. As a result, PCI DSS Requirement 12.5.2.1 applies and Green-Got confirms PCI DSS scope at least once every six months and after significant changes to the in-scope environment.

Update Triggers

The network diagram is updated when any of the following changes occur:

Update Procedure

  1. The Core Banking Team identifies whether a planned or completed change affects CDE network boundaries, connections, security controls, or administrative access paths.
  2. The Network diagram is updated to reflect the current environment.
  3. The update is checked against 3_cardholder_data_flow.md so that network paths and cardholder data flows remain consistent.
  4. The change is reviewed through the normal pull request process before being merged.
  5. The reviewer verifies that the diagram includes the current update date and that the pull request history records the person who made and approved the change.

Revision History

Revision history for the cardholder network diagram is maintained through the Git history for network_diagram/index.md and the pull requests that update it. Each change records the date, author, reviewer, and description of the update. This provides the auditable history required to demonstrate that the diagram remains accurate after significant CDE changes.

Change management procedures

Provide documentation that demonstrates adherence to established change management procedures for changes to information processing facilities and systems.

Our change management process is documented here

Change Management

Cipher suite and protocol inventory

Provide a documented inventory of all cryptographic cipher suites and protocols currently in use within the environment. This inventory should include details on where and why each cipher/protocol is used and support an annual review process to ensure alignment with industry standards and organizational security policies.

Evidence

The repository currently documents and configures the following cryptographic protocols and cipher suites in active platform flows:

The following cipher suites and encryption algorithms are explicitly represented in the repository and used for these flows:

This inventory is reviewed annually and updated whenever transport security or cryptographic architecture changes.

Completed employee background checks ❌

Evidence of completed background checks for your employees. Note: Only provide this evidence if you’re using a background check vendor with which Vanta is not integrated. A template can be found here: Google docs template

Comprehensive and appropriate asset inventory

Create comprehensive information system inventory document that tracks all assets deemed important to track by the organization not already included in Vanta (See: NIST 800-53 CM-8) including any assets used for business purposes that are not owned by the company including employee devices, vendor devices, etc.

Evidence

All assets are tracked in Vanta Inventory.

Compute environments segregated

Evidence of segregation of compute environments

Evidence

6.5.3 — Pre-production / Production Separation

Green-Got enforces separation between pre-production (staging) and production environments using completely separate AWS accounts. Each environment is managed as an independent Pulumi stack with its own isolated resources including separate VPCs with no network peering or cross-account access, independent IAM policies and credentials, dedicated Aurora database clusters, independent Security Groups, and separate security monitoring (GuardDuty, CloudTrail, WAF).

Administrative access is provided through a unified Tailscale mesh VPN using WireGuard. Although Tailscale operates as a single network, Tailscale ACLs (Grants) enforce strict separation between environments. Access to staging and production resources is controlled independently through these ACL policies, ensuring that access to one environment does not grant access to the other.

Environment Segregation Diagram

A1 — Multi-tenant Service Provider Controls

Appendix A1 requirements are not applicable. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not host distinct customer compute environments.

Configure File Integrity Monitoring solution to detect changes to critical systems and files in the CDE ⏱️

Evidence that FIM solution is enabled and configured to detect and alert on changes to critical systems and files. Guidance: Upload screenshots of FIM configurations showing files and paths monitored at least weekly, and a sample alert generated in response to an unauthorized file change.

Evidence

Host

We are using EC2 for compute with Amazon Linux 2023 as the operating system. On these EC2 machines we are running AWS ECS to deploy our containers.

For file integrity monitoring we use AWS Inspector which provides continuous vulnerability scanning and change detection for our EC2 instances. Inspector automatically detects changes to the system and alerts on potential security issues.

Container

To eliminate the need to run FIM inside our container we enable readonlyRootFilesystem for our container to also make the root filesystem of our container read-only.

Control Over Authentication Factor Usage ❌

Provide documentation demonstrating that only the intended user can activate or use each authentication factor (e.g., smart card, hardware token, certificate).

@chloe

Coordination of security event management with customers is formalized

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.2.2 and A1.2.3 apply to multi-tenant service providers, requiring formalized processes for customer-specific forensic investigations and coordinated reporting of security incidents and vulnerabilities. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not manage separate customer environments.

Green-Got’s incident response and forensic investigation processes apply to its own PCI DSS environment and are documented in the internal incident response evidence set.

Critical documentation updates completed

Provide evidence that after significant changes applicable critical documentation is updated within a reasonable timeframe. Guidance: Things like Network diagrams, Risk assessments, Process documentation, etc is often not updated after critical or significant changes are made resulting in a disconnect between the reality of an environment and its representation in documentation. Provide an example of a time when a large change was made to the environment and supporting documentation was updated within a reasonable timeframe - ideally days to weeks not weeks to months.

Evidence

Co-located documentation

All critical documentation — including network diagrams, data-flow diagrams, risk assessments, process documentation, infrastructure configuration, and PCI DSS compliance evidence — is stored directly inside the same Git repository as the application source code. This co-location eliminates the gap between implementation and documentation by design.

Why documentation stays in sync with changes

Because documentation lives alongside the code it describes, any change to the environment — whether it affects infrastructure, network security controls, cardholder data flows, or application logic — is naturally submitted together with the corresponding documentation update in a single pull request. This co-location makes it easy and intuitive for developers to update documentation as part of the same change, significantly reducing the likelihood of documentation drift.

Every pull request goes through the established change management workflow before it is merged:

  1. Change management review — All changes require approval from at least one reviewer before merging. Domain-specific subject matter experts (SMEs) are assigned to relevant areas of the codebase, including documentation directories. Reviewers verify that documentation is updated alongside the implementation and flag pull requests where documentation updates are missing.

  2. AI-based review — AI coding agents (Claude Code, OpenCode) are integrated into the development workflow and configured with repository-wide instructions that enforce documentation discipline. These agents flag discrepancies between code changes and documentation, and call out when a change does not include corresponding documentation updates. This automated review acts as an additional safety net that catches documentation gaps before the review process begins.

Outcome

The combination of co-located documentation, mandatory pull request review, CODEOWNERS enforcement, and AI-assisted review significantly reduces the risk of documentation drift. Because documentation and implementation are part of the same repository and review process, updates typically happen at the same time as the change they describe — not days or weeks later.

Critical security patches are installed within one month of release

Evidence that critical security patches are installed within one month of release on PCI-impacting systems. Guidance: Upload evidence demonstrating that critical security patches have been installed. This is typically a status screenshot from an automated patch management system or results from recent technical vulnerability reports showing no out-of-date patches are present.

Critial security patches in code that runs inside a container

We use Dependabot to notify us about available security patches in our code. As visible in the screenshot we make sure to deploy them as soon as possible.

Dependabot Overview Dependabot Alert

Critial security patches in code that runs on the host

The EC2 host system automatically get’s new security patches applied since we are using AWS Autoscaling Instance Refresh to rotate machines every week. In this process the new machines that are rotated in have the latest security patches installed. We are replying on AWS Machine images to always have the newest version available.

Cryptographic functions are outsourced to a compliant Service Provider ⏱️

Evidence that cryptographic functions related to the protection of stored, processed or transmitted card data such as tokenization, hashing, encryption, etc are covered via vendor(s) Attestation of Compliance.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-04-30
Document OwnerPlatform / Security
Review FrequencyAnnual or after a material third-party cryptographic service change

Green-Got uses a combination of internal cryptographic controls and third-party cryptographic services for card data protection. Internal application cryptography is documented separately in the cardholder data and key-management evidence. This evidence tracks the vendor attestations that cover outsourced cryptographic functions or vendor-operated cryptographic environments.

Vendor / service providerCryptographic function in scopeEvidence to attach in VantaCurrent evidence status
AWSAWS KMS protects Green-Got data-at-rest key-encrypting keys and KMS-generated data keys. AWS Systems Manager Parameter Store protects the PAN lookup HMAC secret values at rest and during authorized delivery to the application at startup. AWS Payment Cryptography provides HSM-backed payment cryptographic operations for PIN, PVV, EMV, and payment-network key material. The keyed HMAC-SHA256 PAN lookup operation itself is performed inside the Green-Got application and is documented as an internal cryptographic control.AWS PCI DSS Attestation of Compliance, AWS PCI DSS responsibility matrix, and AWS services-in-scope evidence covering AWS KMS, AWS Systems Manager Parameter Store, and AWS Payment Cryptography.AWS AOC evidence is maintained in Vanta vendor management. The service-scope evidence for AWS KMS, AWS Systems Manager Parameter Store, and AWS Payment Cryptography must be attached to this document.
ArkéaArkéa acts as banking and payment partner for issuer connectivity and provides or governs payment cryptographic key material used for Mastercard and card-domain operations.Arkéa PCI DSS AOC or equivalent compliance attestation, plus contract or responsibility evidence covering payment cryptographic key exchange and issuer processing responsibilities.To attach from Vanta vendor management.
MastercardMastercard provides payment-network services and network cryptographic requirements for authorization traffic, including card transaction transport and PIN-block related network flows.Mastercard compliance evidence or network responsibility documentation demonstrating the PCI status and cryptographic responsibilities applicable to issuer transaction processing.To attach from Vanta vendor management or Mastercard program evidence.
ExceetExceet performs physical card manufacturing and personalization. Green-Got sends protected personalization data and manufacturer PIN block material for physical cards.Exceet PCI DSS AOC or equivalent compliance attestation covering card personalization, card data handling, and cryptographic handling of personalization payloads.To attach from Vanta vendor management.
ApataApata provides 3-D Secure services and validates authentication values for e-commerce card transactions.Apata PCI DSS AOC or equivalent compliance attestation covering 3DS processing and authentication-value handling.To attach from Vanta vendor management.

Carte Bancaire is not listed as a direct service provider for this evidence because Green-Got’s card-network relationship is mediated through Arkéa for the current scope.

Supporting internal documents:

Change History

VersionDateAuthorChanges
1.02026-04-21enrico@green-got.comDocument outsourced cryptographic function coverage and service-provider evidence.
1.12026-04-30julian@green-got.comClarify that PAN lookup HMAC generation is an internal application control and that AWS covers secret storage and delivery rather than outsourced HMAC computation.

Cryptographic response plan ❌

Provide documentation outlining your organization’s plan for responding to newly discovered cryptographic vulnerabilities or deprecations. The plan should describe how risks are evaluated, decisions are made, and actions are taken to mitigate or phase out weak or outdated cipher suites and protocols. Google Docs Template / Docx Template

Evidence

Cryptographic Failure Response Plan

(in accordance with PCI DSS v4.0.1 Requirement 12.3.3)

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-05-01
Document OwnerEnrico Schaaf (Senior Software Developer)
Review FrequencyAnnually or after crypto-event

1. Purpose

To define a structured process for identifying, responding to, and mitigating failures in cryptographic protection mechanisms that protect cardholder data (CHD) and sensitive authentication data (SAD). This includes compromised keys, deprecated cipher suites, CVEs in cryptographic libraries, and implementation flaws.


2. Scope

This plan applies to all cryptographic technologies used across the CDE and supporting systems:

TechnologyUsageAlgorithm
AWS KMS (Envelope Encryption)KEK for all data-at-rest encryption of CHD/SAD in PostgreSQLAES-256 (KEK), ChaCha20-Poly1305 (data encryption)
ChaCha20-Poly1305Local authenticated encryption of PAN, PIN, CVC, cardholder metadata via KMS-generated data keysAEAD (256-bit key, 96-bit nonce, 128-bit tag)
HMAC-SHA256PAN matching/lookup index without decryptionApplication-side HMAC-SHA256 using startup-loaded 256-bit keys from AWS Systems Manager Parameter Store, with a 12-month cryptoperiod and maximum 30-day rotation window
AWS Payment CryptographyPartner communication with Arkéa-provided keys (Exceet, Mastercard, Apata)TDES (required by partner protocol), AES-256, HMAC-256 (3DS AAV)
TLS 1.3All data-in-transit encryption. TLS 1.3 is enforced across all layers: CloudFront (TLSv1.3_2025 policy), ALB (ELBSecurityPolicy-TLS13-1-3-2021-06), and application-level HTTP clients. TLS 1.2 capability exists in outgoing HTTP client dependencies but is not actively usedTLS 1.3
x.509 CertificatesTLS certificates for public endpointsManaged via AWS Certificate Manager

For full cryptographic architecture details, see Cryptography & Key Management for the Card Domain.


3. Trigger Events

TriggerExampleDetection Source
CVE in cryptographic libraryVulnerability in a cryptographic dependency (e.g., ChaCha20-Poly1305, TLS, or AWS SDK libraries)GitHub Dependabot alert, custom CI Dependency Scan check using cargo-deny on dependency-impacting PR
Compromised encryption keyUnauthorized KMS Decrypt call, leaked data key materialAWS CloudTrail alerts in ClickStack
Compromised PAN lookup HMAC secretUnauthorized retrieval of PAN lookup HMAC Parameter Store values, unexpected dual-slot mismatch, failed rotation verificationAWS CloudTrail alerts for AWS Systems Manager Parameter Store, application rotation metrics, change review
Deprecated cipher suiteCVE deprecating ChaCha20-Poly1305 or TDES weakness escalationGitHub Dependabot, CVE feeds
Certificate expiration or trust-chain failureTLS certificate nearing expiry or invalid chainAWS Certificate Manager monitoring, ClickStack alerts
Configuration driftUnencrypted traffic detected, TLS downgradeClickStack alerting (ALB logs), SigNoz alerting (HTTP 5xx, connection errors)
KMS, Parameter Store, or Payment Cryptography service disruptionAWS KMS API errors, Parameter Store retrieval failures, elevated latency on cryptographic callsClickStack alerts (CloudTrail), SigNoz alerts (application errors), AWS Health Dashboard
Key rotation overdueScheduled rotation interval exceeded for KMS KEK, PAN lookup HMAC startup secrets, or Arkéa-provided keysManual review (annual), rotation tracker, AWS KMS automatic rotation tracking

4. Cryptographic Failure Response Workflow

Green-Got operates as a small team without a dedicated SOC, CISO, or Configuration Manager. All team members share security responsibilities. Enrico Schaaf (Senior Software Developer) acts as the primary decision-maker for cryptographic incidents.

StepActionOwner
1. DetectionIdentify failure via ClickStack alerts, SigNoz alerts, GitHub Dependabot, CVE feeds, or manual reviewAny engineer
2. ContainmentIsolate affected system: disable compromised key or secret access in AWS KMS, AWS Systems Manager Parameter Store, or AWS Payment Cryptography, and block traffic if unencrypted exposure is detectedAny engineer
3. AssessmentEvaluate scope (which CHD/SAD affected), impact (data exposure risk), and compliance implications. Determine if Arkéa-provided keys are involved (requires coordination with Arkéa)Enrico Schaaf
4. CommunicationNotify full engineering team via Slack (#devops). If CHD exposure is confirmed: notify Arkéa, legal counsel, and Vanta compliance contactEnrico Schaaf
5. Key/Cipher RotationRotate affected keys: (a) AWS KMS — trigger key rotation and re-encrypt affected data, (b) PAN lookup HMAC secret — write a new secondary Parameter Store secret, redeploy, backfill the oldest dated lookup slot, complete verification within the rotation window, and retire the previous secret, (c) Arkéa keys — coordinate with Arkéa for new ZMK/wrapped keys, (d) update affected cryptographic dependency if CVE-drivenEngineer assigned
6. Patch or ReconfigurationUpdate affected dependency, deploy patched build via standard change management (PR, review, merge, deploy). For simple dependency updates, the automated GitHub Dependabot pipeline opens a PR that any engineer reviews and merges. For TLS: update configuration via PulumiEngineer assigned
7. ValidationRun build and tests, verify encryption/decryption round-trips in staging before production deploy. Confirm via ClickStack and SigNoz that no error spikes post-deployEngineer assigned + reviewer
8. DocumentationLog the event in a GitHub issue. Record: timeline, root cause, affected systems, actions taken, and lessons learnedEnrico Schaaf
9. Post-Mortem ReviewReview as a team. Update cryptographic inventory if keys or algorithms changed. Update this plan if process gaps are identifiedFull team

5. Tools & Inputs

Vulnerability Management Tools

SIEM / Alerting

KMS / Secret Management / HSM Logs

Crypto Inventory Logs

Policy References


6. Reporting & Audit Trail Requirements

Every cryptographic failure event is documented in a GitHub issue with the following fields:

FieldDescription
Detection timestampDate/time the failure was first detected
Detection sourceClickStack alert, SigNoz alert, Dependabot alert, CVE feed, manual discovery
Affected systemsWhich components: KMS keys, Payment Cryptography keys, cryptographic libraries, TLS config, application services
Data at riskWhich CHD/SAD was potentially exposed (PAN, PIN, CVC, cardholder metadata)
Root causeCVE, configuration error, key compromise, library vulnerability
Risk ratingCritical / High / Medium / Low
Containment timeTime from detection to isolation of affected system
Remediation timeTime from detection to full fix deployed in production
Actions takenKey rotation, dependency update, config change, re-encryption
Reviewed byEnrico Schaaf sign-off

ClickStack retains infrastructure and audit log data (CloudTrail events, ALB logs, database logs) as append-only audit evidence in ClickHouse. SigNoz retains application logs, traces, and metrics.


7. Plan Review & Approval

This plan is reviewed annually or immediately after any cryptographic failure event. Updated when the crypto inventory or tooling changes (e.g., library upgrade, new KMS region, alerting rule changes).

NameRoleDate
Enrico SchaafOwner + Approver
Second EngineerPeer Reviewer

Change History

VersionDateAuthorChanges
1.02026-04-05enrico@green-got.comDocument the cryptographic failure response process for PCI DSS evidence.
1.12026-04-30julian@green-got.comReplace AWS KMS HMAC lookup references with application-managed PAN lookup HMAC secrets stored in AWS Systems Manager Parameter Store and add document control metadata.
1.22026-05-01julian@green-got.comClarify PAN lookup HMAC response-plan rotation with timestamp-based lookup-slot backfill.

Custom code follows a formal change control process

Evidence that a formal change control process is followed for PCI-impacting custom code changes. Guidance: Upload sample tickets demonstrating that PCI-impacting changes include the following:

Evidence

Code changes of any kind follow the default change control process that is already documented

Custom code is reviewed prior to release to production or customers to identify any potential coding vulnerabilities

Evidence that custom code impacting cardholder data is reviewed for security vulnerabilities prior to release.

Guidance:

All custom code impacting cardholder data must be reviewed for security vulnerabilities prior to release to ensure that there are no issues impacting cardholder data security.

If issues are identified (e.g. those identified as OWASP-impacting or “high” vulnerabilities as defined by Requirement 6.1): remediate and retest until the issues are resolved, and include results of the retest in the change control ticket.

If the OWASP checklist is not used: include manual or automated results from a code review (such as code analysis tool output) demonstrating that testing for vulnerabilities was done prior to release by someone other than the original author, issues were corrected and retested, changes were applied to all relevant PCI systems, and all “high” and OWASP-impacting vulnerabilities were included in the testing.

If custom code impacting CHD is developed, update Appendix C: Secure Development Standards in the PCI Policy.

If no code is developed that impacts cardholder data, use the ‘Deactivate’ button and include an explanation.

Evidence

Code changes of any kind follow the default change control process that is already documented

Custom developed software inventory

Provide a list of all in-scope bespoke and custom software, and third-party software components incorporated into bespoke and custom software.

Evidence

We have a single monolith which is our in-scope bespoke and custom software. The repository is available here Github Repository The repository also contains an up to date Software bill of materials in form of a Cargo.lock and a Dockerfile

Customer access restricted to their own data

Examine system, access, and/or network configurations demonstrating that customers have privileges established to only access their own account data and CDE and can only access system resources as expected.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.1.2 and A1.1.3 apply to multi-tenant service providers that allocate separate environments, account data, and system resources per customer. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not allocate distinct customer CDEs.

Green-Got’s customers interact with its banking service as end users. They do not receive direct privileges to separate environments or system resources under this control.

Customer account access request

Provide documentation showing how customer account access requests are submitted, reviewed, and fulfilled. This includes any request made by a customer to access, modify, or recover their user account, as well as the internal process for verifying and granting such access securely.

Evidence

Customer account access and modification requests are handled through authenticated self-service flows where the customer-facing application supports the requested action. When a customer request requires operational assistance, the request is fulfilled through the Green-Got Back Office.

Back Office permissions are assigned by role. Operators with the required role permission perform the customer-account action directly in the Back Office. Operators without the required permission cannot perform the restricted action themselves; they create an escalation request for an operator with the required permission.

The request queue separates incoming requests that require review from outgoing requests created by the current operator.

Pending escalation requests

A reviewer opens the request detail view before making a decision. The request record includes the requester, requester comment, user or account context, requested action, reason, amount where relevant, and current request status.

Pending request details

Approval is an explicit reviewer action. The confirmation step identifies the requester and the action before the reviewer confirms the approval.

Approve request confirmation

After approval, the request detail view records the approved status and the operator who approved the request.

Approved request details

Back Office actions are recorded in the audit history and are attributable to the individual operator who performed the action. The supporting evidence for unique back-office user identifiers, session binding, audit entries, validation requests, and role-based access control is documented in User account non-repudiation.

Access governance for this process is defined in the Access Control Policy. The policy requires access to be based on least privilege and role-based access control, and requires access requests and rights modifications to be documented in Linear or Slack with approval from the system owner, data owner, or management.

Customer responsibilities

Upload or link to documentation (e.g. responsibilities matrix) which communicates to customers their responsibilities for maintaining the security of their account or service.

Evidence

Green-Got operates as a banking institution providing financial services directly to retail consumers and business customers. Green-Got does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers to build upon. All of Green-Got’s customers are end-users of its banking services, not merchants or service providers relying on Green-Got to satisfy their own PCI DSS obligations.

PCI DSS Requirement 12.9.2 applies to TPSPs that support their customers’ PCI DSS compliance by providing information needed for Requirements 12.8.4 and 12.8.5. Those requirements govern PCI-assessed entities managing third-party service provider relationships. Green-Got’s customers do not maintain such responsibilities, as they consume banking services as end-users rather than integrating them into their own cardholder data environments.

Data deletion chd ⏱️

Evidence that there is an automated or manual process to delete stored cardholder data at least every 90 days. Updates made to Data Management and PCI Policies. Guidance: Upload screenshot of script or code snippet demonstrating CHD deletion process. If no CHD is stored, use the ‘Deactivate’ button with an explanation. and update the Data Management Policy (Appendix B) and PCI Policy to specify that no CHD is allowed to be stored.

Evidence

As of now we are only a card issuer not a payment gateway or a merchant. So the CDH we are storing is our own cards which we need to keep indefinitely. If we implement a payment gateway and start storing CDH of cards we did not issue we will update this documentation.

Data is retained according business, legal, and regulatory requirements ❌

Provide screenshots of configuration settings, queries with outputs, and related logs showing:

  1. Sensitive data is retained across each in-scope system according to the requirements defined in your Data Management / Retention policy.
  2. Sensitive data is kept for no longer than the retention periods defined in your data retention matrix.
  3. Sensitive data stored is limited to necessary amounts of data required to perform the service functionality. Guidance: Consider requirements for your policies, procedures, risk assessment records, data stores, assets, personnel information, and disclosures related individual sensitive data (such as PHI or PII). For example, in healthcare organizations, this data much be retained for a minimum of 6 years. Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% of your in-scope systems that store your sensitive data, as randomly selected and requested by your auditor.

Database access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login attempts on 3 (or all if fewer than 3) different databases using the vendor-supplied default accounts and passwords. The evidence must specify the default accounts/passwords used.

Evidence

Amazon Aurora does not provide vendor-supplied default passwords. Database credentials are defined at cluster creation time by the customer, and no default credentials (e.g., “admin/admin”) are enabled by default. AWS documentation

Access to the database is restricted through network controls (VPC security groups) and authentication mechanisms (strong passwords and/or IAM authentication).

As no vendor-supplied default credentials exist, there are no default accounts or passwords to test against. Unauthorized access attempts result in authentication failures enforced by the database engine.

Deprecated NSC configurations

Please provide an example of a Network Security Control configuration that is no longer in use.

Evidence

Network Security Controls are managed via infrastructure as code. So if a control gets deprecated it can be tracked via the git history. As of now we did not yet deprecate any NSC.

Developers of PCI-impacting code are knowledgeable in secure coding techniques

Evidence that developers attend training in up-to-date secure code training and awareness activities at least annually. Guidance: Provide evidence (such as attendance rosters or certifications) demonstrating that developers who create or modify PCI-impacting code are trained in secure coding techniques at least annually. If no code is developed that impacts cardholder data, use the ‘Deactivate’ button and include an explanation.

Evidence

Green-Got assigns Vanta’s built-in Secure code training to all developers who create or modify PCI-impacting code through the Developers group. This training covers up-to-date secure coding techniques.

Training is delivered at least annually. Individual completion is tracked in Vanta People. The training content available by framework is documented in Vanta’s training video access reference.

Disclosure of internal IPs approved

Evidence of approval prior to sharing internal IP addressing. Guidance: If internal addresses are shared with external parties, provide evidence that the disclosure was explicitly approved (ticket, email, or up-to-date inventory of vendors with sharing of IPs denoted). If internal addresses are not shared with external parties, use the ‘Deactivate’ button and include an explanation.

Evidence

Our infrastructure uses two categories of IP addresses that are shared with external parties. In both cases, disclosure of these addresses does not weaken our security posture because our security controls do not rely on the secrecy of IP addresses (i.e., we do not depend on security through obscurity).

1. VPC private IP addresses

Our AWS VPC uses private IP address ranges (RFC 1918). These addresses are shared with Mastercard as part of network integration requirements. These addresses are non-routable on the public internet and are only reachable within the VPC or through explicitly configured private connectivity. Knowledge of these addresses by an external party provides no attack surface because they are unreachable from the public internet.

IP typeShared withReason for sharing
VPC private addresses (RFC 1918)MastercardNetwork integration requirements

2. External (egress) IP addresses

We maintain a set of external IP addresses used as source addresses for outbound requests from our infrastructure. These addresses are shared with partners that enforce IP-based allowlisting on their endpoints, including Hawk, Apata, and Exceet.

These egress IP addresses are published via a public DNS A record at egress.ips.green-got.co (TTL 300s). This record is automatically populated with all Elastic IP addresses allocated across our AWS regions. Partners query this DNS record to discover our current egress IPs and update their allowlists accordingly — for example, when a new IP address is added during infrastructure scaling.

IP typeShared withReason for sharing
Egress IP rangeHawkIP allowlisting on their endpoints
Egress IP rangeApataIP allowlisting on their endpoints
Egress IP rangeexceetIP allowlisting on their endpoints

Why formal approval is not necessary for these disclosures

Neither category of IP address constitutes sensitive internal addressing whose disclosure requires prior approval:

Security posture

Our security posture does not depend on the secrecy of any IP address:

We do not rely on security through obscurity. Restricting knowledge of these IP addresses provides no meaningful security benefit, and therefore no formal approval process is required for their disclosure.

Disk encryption config

Evidence that access to CHD is not granted via native OS or local user accounts and keys are stored on a secure system. Guidance: This control applies only if hard disk encryption is used to protect CHD on systems and removable media (not typical). If you do not store CHD or protect stored CHD using disk encryption, use the ‘Deactivate’ button and include an explanation.

Reason for deactivation

Whenever CHD is stored on disk, the disk and its access is managed by AWS since we are using AWS Aurora and AWS S3 so we delegate this responsibility. In addition CD is encrypted with our own encryption key as well so we are not relying on disk encryption.

Document and manage processes for protection of clear-text and manually generated keys

Evidence that manual clear text keys are protected via split knowledge, dual control, and cannot be substituted by an individual. Guidance: this is not typical, and applies only to entities generating their own static manual keys. If manual keys are not used or generated, use the ‘Deactivate’ button and include an explanation.

Evidence

Changes to keys follows the default management workflow. So changes to keys are also subject to review and approval. This means no single individual can substituted a key.

Document key management processes and protect keys used for encryption of CHD ❌

Evidence that encryption processes are documented and understood. Guidance: Provide evidence of key strength and locations of key storage. This can include vendor documentation if cloud service provider or HSM is used for key management activities but should also include a process document of some type if key management is not totally automated via system such as AWS KMS. PCI Specific: If CHD is not stored and encryption is not used to protect CHD, use the ‘Deactivate’ button and include an explanation.

Email protection

Provide evidence that company incoming email is scanned and protected from common malware, phishing, and other attacks.

Evidence

We are using Gmail as part of Google Workspace. As visible in the screenshot we have “Enhanced pre-delivery message scanning” enabled.

Screenshot Google Admin

Encryption Key Inventory Maintained ⏱️

Provide a screenshot or export from your key management system (KMS) showing all active and inactive encryption keys for your in-scope systems, including metadata such as key IDs, purpose, status, owner, as well as retention & rotation schedules. Guidance: Implement a centralized key management solution that supports tagging, auditing, and lifecycle tracking of keys. Document all keys in use across both your cloud services and any on-premise systems and then assign ownership for each key.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-06-08
Document OwnerPlatform / Security
Review FrequencyAnnual or after a material cryptographic inventory change

Green-Got maintains the encryption key inventory through AWS KMS, AWS Systems Manager Parameter Store, AWS Payment Cryptography, infrastructure-as-code definitions, and environment configuration. AWS KMS exports are the authoritative evidence for KMS-managed keys. AWS Systems Manager Parameter Store metadata is the authoritative evidence for the PAN lookup HMAC secrets loaded by the application at startup. AWS Payment Cryptography exports are the authoritative evidence for active/inactive status, creation date, last rotation date, next rotation date, key state, and planned deletion date for payment cryptography keys.

Production and non-production PAN lookup HMAC secrets are maintained as separate secrets in separate AWS accounts. The same PAN lookup HMAC key material is not reused across production and non-production environments.

AWS KMS keys used for cardholder-data protection:

EnvironmentKey classAWS accountRegion / scopeKey ARN or identifierAlias / resource namePurposeOwnerRotation / retention
ProductionStorage KEK489754752373eu-central-1 primaryarn:aws:kms:eu-central-1:489754752373:key/mrk-54ffeefb6b7c4a78afb967dd95d3cf66alias/encryption-keyWraps KMS data keys used for application-side ChaCha20-Poly1305 storage encryptionPlatform / SecurityAWS KMS automatic rotation enabled; 30-day deletion window
ProductionStorage KEK replica489754752373eu-west-3 replicaarn:aws:kms:eu-west-3:489754752373:key/mrk-54ffeefb6b7c4a78afb967dd95d3cf66alias/encryption-keyRegional continuity for the storage KEKPlatform / SecurityFollows multi-region KMS key lifecycle; 30-day deletion window
Non-productionStorage KEK215052876796eu-central-1 primaryarn:aws:kms:eu-central-1:215052876796:key/mrk-2ca98a2781e04294bd3c46ae5591f8aaalias/encryption-keyStaging and local-development storage encryption test keyPlatform / SecurityAWS KMS automatic rotation enabled; 30-day deletion window
Non-productionStorage KEK replica215052876796eu-west-3 replicaarn:aws:kms:eu-west-3:215052876796:key/mrk-2ca98a2781e04294bd3c46ae5591f8aaalias/encryption-keyRegional continuity for the non-production storage KEKPlatform / SecurityFollows multi-region KMS key lifecycle; 30-day deletion window
Production and non-productionDatabase volume keyPer AWS account and regioneu-central-1, eu-west-3Attach AWS KMS inventory export value for KeyArndbKmsKeyAurora/RDS database volume encryptionPlatform / SecurityAWS KMS automatic rotation enabled
Production and non-productionRoute 53 DNSSEC signing keyPer AWS accountus-east-1Attach AWS KMS inventory export value for KeyArnDNSSEC signing keyPublic hosted-zone DNSSEC signingPlatform / SecurityRoute 53 DNSSEC lifecycle; 7-day deletion window

AWS Systems Manager Parameter Store secrets used for PAN lookup HMAC operations:

EnvironmentSecret classAWS accountScopeParameter identifierPurposeOwnerRotation / retention
ProductionPAN lookup HMAC active secret489754752373Production application configurationAttach Parameter Store metadata for the active production SecureString valueActive HMAC-SHA256 key used by the application for keyed PAN lookup value generation and verificationPlatform / Security256-bit key, 12-month cryptoperiod from activation date, maximum 30-day rotation window for backfill, verification, and previous-key retirement
ProductionPAN lookup HMAC secondary secret489754752373Production application configurationAttach Parameter Store metadata for the secondary production SecureString valueSecondary HMAC-SHA256 key loaded during rotation for timestamp-based dual-slot backfillPlatform / Security256-bit key, staged only for the current rotation, must not remain active beyond the 30-day rotation window
Non-productionPAN lookup HMAC active secret215052876796Non-production application configurationAttach Parameter Store metadata for the active non-production SecureString valueActive non-production HMAC-SHA256 key used by the application for keyed PAN lookup value generation and verificationPlatform / Security256-bit key, separate from production, rotated under non-production change management
Non-productionPAN lookup HMAC secondary secret215052876796Non-production application configurationAttach Parameter Store metadata for the secondary non-production SecureString valueSecondary non-production HMAC-SHA256 key loaded during rotation testing and validationPlatform / Security256-bit key, separate from production, used only for non-production rotation testing and validation

AWS Payment Cryptography keys used for card payment operations:

Key classInventory identifierPurposeOwner / sourceOperational status sourceRotation / retention
ZMKAWS Payment Cryptography key ARN after importWraps and unwraps Arkéa-provided TR-31 key blocksArkéa key authority; Green-Got imports into AWS Payment CryptographyAWS Payment Cryptography exportCurrent production AES-256 ZMK retained through end-2030 per Arkéa lifecycle; replacement ceremony before expiry or earlier if required
IMK-ACimkac_tr31_dki_f1 / imported key ARNApplication cryptogram validation and generation supportArkéa; shared with Exceet for personalizationAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
IMK-IDNimkidn_tr31_dki_f1 / imported key ARNICC dynamic number supportArkéa; shared with Exceet for personalizationAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
IMK-SMCimksmc_tr31_dki_f1 / imported key ARNSecure messaging confidentiality for issuer scriptsArkéa; shared with Exceet for personalizationAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
IMK-SMIimksmi_tr31_dki_f1 / imported key ARNSecure messaging integrity for issuer scriptsArkéa; shared with Exceet for personalizationAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
ZMK_APATAapata_zmk_arnWraps the KAAV export into the TR-31 block transmitted to Apata; Green-Got does not transmit ZMK material to ApataArkéa key authority; distributed by Arkéa for Apata exchange; Green-Got imports into AWS Payment CryptographyAWS Payment Cryptography exportCurrent production AES-256 ZMK material retained through end-2030 per Arkéa lifecycle; replacement ceremony before expiry or earlier if required
KAAV_APATA_EXPORTapata_export_kaav_tr31 / apata_export_kaav_key_arn3D Secure AAV/CAVV validation key generated by Green-Got and exported under the Arkéa-distributed Apata ZMK for partner distributionGreen-Got; exported KAAV TR-31 block shared with ApataAWS Payment Cryptography exportPer Arkéa / Apata 3DS schedule
KCVVkcvv_tr31_idx1 / imported key ARNCVV/CVC computation and validationArkéa; Green-Got onlyAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
KPVVkpvv_tr31_idx1 / imported key ARNPVV computation for online PIN verificationArkéa; Green-Got onlyAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
PEK_EXCEETexceet_pek_tr31 / imported key ARNPIN block protection for Exceet manufacturing and personalizationArkéa; shared with ExceetAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement
PEK_MASTERCARDmastercard_pek_tr31 / mastercard_pek_key_arnPIN block protection for Mastercard authorization-system PIN encryption and decryption flowsArkéa; Green-Got onlyAWS Payment Cryptography export3-year issuance cryptoperiod; authorization use retained for non-expired cards across the 8-year retention horizon, with an additional 2-3 month rotation period granted by Arkéa during replacement

The TR-31 values present in non-production configuration are test material and are not production key inventory evidence. Production evidence for active or inactive AWS Payment Cryptography keys is the AWS Payment Cryptography export, including key ARN, key state, key usage, key origin, enabled modes of use, creation date, and any planned deletion date.

The active Arkéa-distributed ZMK material is retained under the Arkéa-approved wrapping-key lifecycle. Arkéa confirmed that AES-256 ZMK material has a longer cryptoperiod than the TDES issuing keys and Green-Got retains the current production ZMK material through the end of 2030 to cover the next payment-key renewal. Replacement occurs through a new Arkéa key ceremony before that date or earlier when required by Arkéa or a cryptographic response event. For Apata 3DS exchange, Green-Got imports the Arkéa-distributed Apata ZMK into AWS Payment Cryptography and transmits only the exported KAAV TR-31 block to Apata.

The TDES TR-31 issuing/payment keys use a 3-year issuance cryptoperiod. At the end of the issuance period, Arkéa draws replacement keys and Green-Got imports them into AWS Payment Cryptography before new card personalization moves to the new key set. The previous key set remains available only for authorization, validation, PIN, EMV, and card-lifecycle operations for cards already issued under that key set. This authorization tail covers the validity of non-expired cards across the 8-year retention horizon. Arkéa grants an additional 2-3 month rotation period during replacement to complete partner rollout and avoid personalization interruption.

Supporting source documentation:

Change History

VersionDateAuthorChanges
1.02026-04-21julian@green-got.comDocument in-scope cryptographic key inventory and ownership evidence.
1.12026-04-30julian@green-got.comReplace AWS KMS HMAC inventory references with AWS Systems Manager Parameter Store active and secondary PAN lookup HMAC secrets and add document control metadata.
1.22026-05-01julian@green-got.comClarify PAN lookup HMAC rotation-window usage and timestamp-based dual-slot backfill.
1.32026-05-11julian@green-got.comAlign the AWS Payment Cryptography inventory with the unprefixed Arkea parameter schema, replace legacy KAAV references with the Apata ZMK and exported KAAV material, and add the Mastercard PEK TR-31 material and imported key ARN.
1.42026-06-04julian@green-got.comDocument Arkéa-approved ZMK retention and TR-31 issuing/payment key cryptoperiods.
1.52026-06-08julian@green-got.comClarify the Arkéa-distributed Apata ZMK source, lifecycle, KAAV TR-31 transmission boundary, and Arkéa-granted TR-31 rotation period.

Ensure Secure Development Policy addresses Auth Bypass

Ensure Secure Development Policy addresses methods and mechanisms in place to prevent Authorization Bypass attacks

Evidence

The Secure Development Policy addresses authorization bypass prevention in the following sections:

Developer Training (Section “Developer Training”): The policy mandates that all software developers receive secure development training at least annually. The training content explicitly includes “prevention of authorization bypass attacks” as a required topic.

Secure-by-design principles (Section “Secure System Engineering Principles”): The policy establishes principles that directly mitigate authorization bypass, including:

Ensure Secure Development Policy addresses Cross Site Request Forgery Attacks

Ensure Secure Development Policy addresses methods and mechanisms to prevent Cross Site Request Forgery Attacks

Evidence

The Secure Development Policy addresses CSRF/XSRF attack prevention in the following sections:

Developer Training (Section “Developer Training”): The policy mandates that all software developers receive secure development training at least annually. The training content explicitly includes “prevention of cross-site request forgery attacks” as a required topic.

System Security Testing (Section “System Security Testing”): The policy requires that testing of security functionality is performed automatically before being deployed to production systems. No security-related code is deployed to production without documented, successful test results and evidence of security remediation activities.

Ensure Secure Development Policy addresses Cross Site Scripting Attacks

Ensure Secure Development Policy addresses mechanisms and methods to prevent Cross Site Scripting Attacks

Evidence

The Secure Development Policy addresses XSS attack prevention in the following sections:

Developer Training (Section “Developer Training”): Software developers are provided secure development training appropriate to their role at least annually. The required training topics include prevention of common web application attacks and vulnerabilities, including prevention of cross-site scripting attacks.

System Security Testing (Section “System Security Testing”): Testing of security functionality is performed automatically before deployment to production systems. Security-related code is deployed to production only with documented, successful test results and evidence of security remediation activities.

Application Vulnerability Management (Section “Application Vulnerability Management”): Application code is scanned prior to deployment. Patches that address application vulnerabilities materially impacting security are deployed within the remediation timeline defined in the Operations Security Policy.

Ensure Secure Development Policy addresses Injection Attacks

Ensure Secure Development Policy addresses methods and mechanisms to prevent Injection Attacks

Evidence

The Secure Development Policy addresses injection attack prevention in the following sections:

Developer Training (Section “Developer Training”): The policy mandates that all software developers receive secure development training at least annually. The training content explicitly includes “prevention of Injection attacks” as a required topic.

System Security Testing (Section “System Security Testing”): The policy requires that testing of security functionality is performed automatically before being deployed to production systems. No security-related code is deployed to production without documented, successful test results and evidence of security remediation activities.

Application Vulnerability Management (Section “Application Vulnerability Management”): The policy states that application code is scanned prior to deployment, and patches to address application vulnerabilities that materially impact security are deployed within the remediation timeline defined in the Operations Security Policy.

Ensure Secure Development Policy addresses Insecure Session IDs

Ensure Secure Development Policy addresses methods and mechanisms to avoid or prevent Insecure Session IDs

Evidence

The Secure Development Policy addresses insecure session ID prevention in the following sections:

Developer Training (Section “Developer Training”): The policy mandates that all software developers receive secure development training at least annually. The training content explicitly includes “prevention of the use of insecure session IDs” as a required topic.

Secure-by-design principles (Section “Secure System Engineering Principles”): The policy establishes principles that directly support secure session management, including:

Ensure Secure Development Policy addresses the use of Vulnerable Libraries

Ensure Secure Development Policy addresses acceptable and unacceptable use of Vulnerable Libraries

Evidence

The Secure Development Policy addresses the use of vulnerable libraries in the following sections:

Developer Training (Section “Developer Training”): The policy mandates that all software developers receive secure development training at least annually. The training content explicitly includes “prevention of the use of vulnerable libraries” as a required topic.

Application Vulnerability Management (Section “Application Vulnerability Management”): The policy states that application code is scanned prior to deployment. Patches to address application vulnerabilities that materially impact security are deployed within the remediation timeline defined in the Operations Security Policy. This covers vulnerabilities introduced through third-party libraries and dependencies.

Acquisition of Third-Party Systems and Software (Section “Acquisition of Third-Party Systems and Software”): The policy requires that the acquisition of third-party systems and software follows the requirements of the Green-Got Third-Party Management Policy, establishing governance over the introduction of external libraries.

Established configuration standard for firewalls and routers

Evidence

Green-Got maintains a documented configuration standard for network security controls in its AWS and Tailscale environment. The environment does not use standalone physical firewalls or routers. Routing is handled by AWS VPC route tables, and enforcement of network access rules is handled by AWS Security Groups, AWS WAF, AWS Shield, and Tailscale Grants.

The network security control standard is implemented through Pulumi-managed infrastructure configuration and applied consistently across the production environment.

Network security controls in scope

Configuration standard

The following standards are defined and implemented for network security controls:

Approved network paths

The following network paths are approved for the production environment:

All other inbound paths are denied by default unless explicitly defined in the active network security control configuration. Outbound traffic is restricted per component: the load balancer permits egress only to application instances, and the database has no outbound rules (implicit deny). Application instances permit unrestricted outbound internet access (all protocols, all ports, all destinations) to support communication with external services such as payment processors, banking partners, and third-party APIs.

Services, protocols, and ports

Allowed services, protocols, and ports are limited to those required for business operation. Each is identified with a business justification:

ServiceProtocolPortBusiness justification
Public application trafficHTTPS443Serves the customer-facing banking application
Mutual TLS integrationsTCP/UDP443Required for approved payment and partner integrations via AWS Global Accelerator
Internal load balancer to applicationHTTP80Routes traffic from the load balancer to application instances within the VPC
Database accessTCP5432PostgreSQL database access from application instances for transaction and account data
Internal application communicationPer security group rulesPer security group rulesEast-west traffic between approved internal components only

No public administrative services such as SSH, Telnet, RDP, or database management ports are exposed to the internet.

Insecure services and protocols

The production environment does not rely on insecure remote administration protocols. Administrative access is performed through Tailscale, and public-facing application access is limited to HTTPS. No insecure public services or protocols are defined as approved entry points in the network security control standard.

Roles of each control

Change control

All changes to network security control configurations follow the formal change control process defined under PCI DSS Requirement 6.5.1. Network security controls are managed as infrastructure as code through Pulumi. Changes are submitted as pull requests, reviewed and approved by authorized personnel before merging, and deployed through the standard release pipeline. Change records are maintained in the code repository and linked ticketing systems.

Configuration review

Network security control configurations are reviewed and approved at least every six months. Reviews confirm that active rulesets match documented business justifications and that no unauthorized, outdated, or unnecessary rules remain in place.

This configuration standard is reflected in the current network diagram and in the active infrastructure configuration used to provision and maintain the environment.

External vulnerability scan

Quarterly external scans covering the external environment are performed by a qualified ASV company. Scans must also be performed ad-hoc after a significant change. Guidance: Entity is responsible for contracting the services of a qualified ASV company, approved by the PCI Security Standards Council. The list can be found at https://www.pcisecuritystandards.org/assessors_and_solutions/approved_scanning_vendors. External scan scope must include all externally facing IPs and services in the CDE. Failing findings must be remediated and a rescan performed until a passing result is achieved. 4Q of attested scan reports must be provided (typically, an entity must formally request quarterly attestation; an attested report will show a “PASS/FAIL” status in the report summary.

Evidence

Green-Got uses Evervault ASV Scans to run quarterly external vulnerability scans, review scan results, and download the attestation and executive summary reports used for PCI DSS Requirement 11.3.2.1. The ASV scan report generated through Evervault identifies Clone Systems, Inc. as the Approved Scanning Vendor performing the scan.

The attached ASV Scan Report is a quarterly external vulnerability scan performed by Clone Systems, Inc. (PCI SSC ASV certificate number 4262-01-18) on February 18, 2026.

Scan result: Pass (full scan). One in-scope component was scanned green-got.co with zero failing vulnerabilities. All noted findings are informational or low severity (highest CVSS score: 2.6 for TCP timestamp detection) and all carry a passing compliance status. The host is protected by a firewall, with only ports 80 and 443 open.

Failed login alerting ❌

Provide evidence that login failures are logged, monitored, and configured to alert the responsible individuals or teams (e.g., system administrators or security operations) following login failures that exceed the number of attempts passing a threshold defined via your internal policy and procedures. Guidance: This threshold is usually defined in your Access Control Policy and any associated Access Control Standards or Procedures. An example threshold could be 5 consecutive failed authentication attempts on an in-scope system.

Failed logins tracked and reviewed ❌

Provide screenshots or other evidence that failed log-in attempts on in-scope systems are logged and able to be reviewed or alerted on via other methods. Guidance: An example would be to have an alert in your SIEM tool trigger if a predefined set of failed logins occur within a defined time-period, notifying a member of the security team via your incident response tool. The logs should contain timestamps and other relevant logs for investigative purposes, and be kept highly available for at least 90 days as a best practice.

Firewall and ACLs reviewed and approved ❌

Firewall or ACL rules are reviewed and approved at least every 6 months. Guidance: Provide sample ticket showing that ACLs/NACLs are reviewed and approved. This can be done by attaching a screenshot or link to the rulebase and generating a ticket assigned to an appropriate team or individual.

Evidence

Recurring Schedule

A recurring issue titled “Perform and document PCI DSS NSC rule review” is configured in Linear under the PCI DSS team. The issue recurs every 6 months, with the next due date shown as October 17.

The recurring issue tracks the scheduled review of network security control rules and configurations, including AWS Security Groups, AWS WAF, AWS Shield, Tailscale Grants, and AWS VPC route tables. The issue records the cadence used to confirm that active network security control rules remain relevant, effective, and supported by a documented business justification.

The recurring issue is tracked in Linear at PCI-14: Perform and document PCI DSS NSC rule review.

The recurring issue configuration is captured in the screenshot below:

Linear recurring issue for NSC rule review every 6 months

Hardcoded credentials not used in scripts

Provide evidence that scripts or other automations used for service accounts that are capable of user-interactive login are not hardcoded. Guidance: Proving a negative can be hard, so an example of a script that correctly uses dynamic access credentials via AWS KMS or a similar method of showing that what you do is not hardcoded credentials is usually acceptable here.

Evidence

Credentials are never hardcoded in the codebase. All secrets are stored encrypted at rest in the repository (ChaCha20-Poly1305) and decrypted at runtime from environment variables injected via AWS SSM Parameter Store. CI rejects any attempt to commit plaintext credentials.

As an example, the Fly.io API client (src/clients/src/fly/fly_client.rs) loads its token from the encrypted PARAMETERS object — not from a hardcoded string:

use env::PARAMETERS;

pub static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
    client_builder()
        .bearer_auth(PARAMETERS.fly_api_token.expose())
        .build()
        .unwrap()
});

All other service credentials (AWS, payment processors, SFTP partners) follow the same pattern: they are accessed via PARAMETERS.<name>.expose(), where each value is an encrypted Secret that is decrypted at startup using a key provided through the runtime environment.

High and critical vulnerability rescan

Provide internal or external vulnerability rescan reports confirming that previously identified high-risk and critical vulnerabilities have been remediated. These reports demonstrate that remediation efforts are validated and that high-severity issues are not left unaddressed in the environment.

Evidence

Green-Got uses GitHub Dependabot to continuously monitor dependencies for known vulnerabilities. When a high or critical vulnerability is identified, Dependabot creates an alert and a pull request is opened to update the affected dependency. Once the fix is merged, the vulnerability is confirmed as resolved. Dependabot was set up in PR #431.

Remediated high-severity vulnerabilities:

AlertSeverityPackageSummaryRemediation PRResolution Time
#28Highlibcrux-sha3Incorrect output from SHAKE squeeze functionsPR #10512 days
#26Highaws-lc-sysCRL Distribution Point Scope Check Logic ErrorPR #9992 days
#25Highaws-lc-sysX.509 Name Constraints Bypass via Wildcard/Unicode CNPR #9992 days
#22Highlz4_flexDecompression can leak uninitialized memoryPR #95814 hours
#21Highquinn-protoUnauthenticated remote DoS via QUIC parameter parsingPR #92512 hours
#20Highaws-lc-sysPKCS7_verify Signature Validation BypassPR #8915 days
#19Highaws-lc-sysTiming Side-Channel in AES-CCM Tag VerificationPR #8915 days
#18Highaws-lc-sysPKCS7_verify Certificate Chain Validation BypassPR #8915 days
#2Highlibymlyaml_string_extend is unsound and unmaintainedPR #73214 hours

All 9 high-severity alerts have been remediated, with an average resolution time of approximately 2 days. The full list of resolved alerts is available at Dependabot alerts.

Hypervisors/Containers access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login attempts on 3 (or all if fewer than 3) different Hypervisors or Containers using the vendor-supplied default accounts and passwords (if any). The evidence must specify the default accounts/passwords used.

Evidence

We use AWS ECS (Elastic Container Service) to orchestrate Docker containers running on AWS EC2 instances. Neither the orchestration layer nor the container images have vendor-supplied default accounts or passwords:

We do not use traditional hypervisors (VMware, Hyper-V, etc.). There are no vendor-supplied default credentials to test against in any of these components.

IDS/IPS capabilities documentation

Provide vendor documentation for your IDS/IPS solution that explains its capabilities for blocking, detection, alerting, etc.

Evidence

AWS GuardDuty documentation

ISMS and security program leadership qualifications

Evidence such as LinkedIn profiles, resumes, certifications, training or education records demonstrating that ISMS or security program leaders are qualified to have decision-making power over the security program. A template can be found here: Google docs template

Evidence

The following individuals have decision-making power over the security program. Their LinkedIn profiles serve as evidence of their professional qualifications, experience, and competence:

External compliance advisor

The following individual from Kobalt advised on the initial PCI DSS audit preparation:

Identifiers auto-disable if not used ❌

Screenshot or other evidence of device administration configurations demonstrating that known identifiers (hostnames, usernames, etc) are disabled in systems and must be re-enabled before use after a defined period of inactivity set via policy. Note: This timeframe is often 30-90 days, but should be defined based on your organizations risk tolerance and any compliance or regulatory requirements that must be met

Iframe web and mobile app scope exclusion

Iframe Scope Exclusion for Web and Mobile Applications

Evidence

This document records the basis for excluding the Green-Got web application shell and mobile application shell from the cardholder data environment (CDE) for iframe-handled cardholder operations.

The exclusion applies to application surfaces where the cardholder views or enters sensitive card data through an isolated iframe served from the Green-Got PCI DSS Level 1 CDE. The iframe application, its hosting origin, backend endpoints, data stores, operational tooling, deployment path, logs, and monitoring remain in PCI DSS scope. The surrounding web and mobile shells are excluded from the CDE only because they do not receive, render, log, persist, transmit, or modify full PAN, CVC, expiry, PIN, or PIN-change values.

This exclusion does not remove the issuer CDE documented for Green-Got backend issuing and Mastercard processing. It defines a customer-facing boundary: the iframe is in scope; the shell is outside the CDE but remains security-impacting.

Scope Basis

PCI DSS v4.0.1 applies to environments that store, process, or transmit cardholder data or sensitive authentication data, and to systems that impact the security of that data. PAN is the defining factor for cardholder data. PIN and CVC are sensitive authentication data.

For the Green-Got iframe flow:

The iframe boundary is valid only when the shell has no technical path to access sensitive iframe content, network responses, storage, logs, or messages.

Cardholder Operations Covered

OperationData involvedScope treatment
Card displayPAN, expiry date, CVCValues render only inside the CDE-hosted iframe.
PIN displayPINValue renders only inside the CDE-hosted iframe after step-up authentication and cardholder authorization.
PIN set or changeNew PIN valueCardholder enters the value only inside the CDE-hosted iframe.
Masked card overviewMasked PAN, card status, non-sensitive card metadataShell displays masked or non-sensitive values outside the CDE.

PIN display and PIN set/change are sensitive authentication data operations. The iframe, backend endpoints, and systems storing or processing PIN values remain in PCI DSS scope.

Architecture

flowchart LR
    User["Cardholder"] --> Shell["Green-Got web or mobile shell"]
    Shell --> Frame["CDE-hosted iframe"]
    Frame --> CDE["Green-Got PCI DSS Level 1 backend"]
    CDE --> Store["Encrypted card data and PIN systems"]

    Shell -. "Non-sensitive state only" .-> CDE
    Frame -. "PAN / CVC / expiry / PIN only inside frame" .-> User
sequenceDiagram
    autonumber
    actor Cardholder
    participant Shell as Web or mobile shell
    participant CDEFrame as CDE-hosted iframe
    participant CDE as Green-Got CDE backend

    Cardholder->>Shell: Opens card operation
    Shell->>CDE: Requests iframe session
    CDE->>CDE: Authenticates user and authorizes card ownership
    CDE-->>Shell: Returns iframe URL and non-sensitive session reference
    Shell->>CDEFrame: Embeds approved iframe origin
    CDEFrame->>CDE: Requests card operation data
    CDE-->>CDEFrame: Returns PAN, CVC, expiry, PIN, or PIN-change context
    Cardholder->>CDEFrame: Views card details or enters PIN change
    CDEFrame->>CDE: Completes operation inside CDE
    CDEFrame-->>Shell: Sends non-sensitive completion state

Mobile Application Treatment

The Green-Got mobile applications do not need a separate PCI DSS AOC, ROC, or standalone mobile PCI certification for this iframe approach. They are covered by Green-Got’s PCI DSS Level 1 program as security-impacting software, not as CDE components, when the following statements remain true:

The mobile shell remains relevant to the PCI DSS assessment because it controls access to the CDE iframe, starts the iframe session, and influences whether the cardholder reaches the genuine CDE origin. The mobile shell therefore requires secure software lifecycle evidence, release controls, dependency management, vulnerability management, and access-control evidence inside Green-Got’s PCI DSS program.

Stripe Benchmark

Stripe’s Issuing documentation provides an external benchmark for this boundary. Green-Got does not rely on Stripe to host this iframe; Stripe is cited because its published model uses hosted iframes to keep sensitive card data away from the integrating application.

Stripe documents the following expectations for Issuing Elements and PIN management:

AreaStripe expectationGreen-Got equivalent
Hosted frame boundarySensitive Issuing card data renders inside hosted iframes and does not touch the integrating server.Sensitive card data renders inside the CDE-hosted iframe and does not touch the shell.
Secure endpointThe server-side endpoint authenticates the requester and authorizes access to the requested card.The CDE backend authenticates the cardholder and authorizes card ownership before iframe access.
Short-lived accessIssuing Elements ephemeral keys expire after 15 minutes.Iframe session authorization is short-lived and bound to the authenticated cardholder session.
PIN displayStripe requires two-factor authentication before pages using PIN display.Green-Got applies step-up authentication before PIN display and PIN set/change.
Native appsStripe directs native apps to load the web integration in a WebView.Green-Got mobile loads the CDE-hosted iframe in a WebView.
PIN set/changeStripe requires PIN set/change to use encrypted PIN submission and does not return the PIN in API responses.Green-Got keeps PIN collection and handling inside the CDE iframe and does not return PIN values to the shell.
Service-provider contextStripe states that user-facing virtual card programs are service-provider candidates and service providers are PCI compliant.Green-Got validates its issuer/CDE environment under its PCI DSS Level 1 program.

Relevant Stripe sources:

SourceRelevance
Using Issuing ElementsHosted iframe display model, ephemeral-key flow, 15-minute expiry, PIN-display 2FA requirement, and native WebView guidance.
PIN managementPIN display, PIN set/change, encrypted PIN submission, no returned PIN in API responses, and online/offline PIN behavior.
Virtual cards with IssuingPCI-DSS treatment for virtual card display and service-provider expectations for user-facing card programs.
Integration security guideShared PCI responsibility, low-risk integrations, TLS, CSP, webhook signature verification, and Stripe PCI Level 1 status.
Security at StripeStripe PCI Service Provider Level 1 certification and PCI validation support by integration method.

Residual Controls

The shell exclusion depends on the following controls:

ControlCurrent state
CDE-hosted frame boundaryPAN entry, card display, PIN display, and PIN set/change occur only inside the CDE-hosted iframe.
No native sensitive fieldsWeb and mobile shells do not implement fields for full PAN, CVC, PIN, track data, card display values, or PIN-change values.
No sensitive telemetryLogs, analytics, crash reports, session replay, screenshots, and support tooling exclude iframe sensitive values.
Non-sensitive parent contractParent-shell endpoints receive iframe URLs, non-sensitive session references, masked metadata, and event results only.
Step-up authenticationPIN display and PIN set/change require step-up authentication before the iframe exposes the operation.
Dedicated iframe originThe iframe uses a dedicated CDE origin restricted by CSP and frame controls.
Shell release controlsWeb and mobile shells are covered by secure software lifecycle, code review, dependency management, and release controls.

Related Green-Got PCI Evidence

EvidencePurpose
2026 H1 PCI DSS Scoping ExerciseRecords the current CDE, in-scope components, out-of-scope components, and the basis for excluding systems that receive only tokens or masked data.
PAN is rendered unreadable anywhere it is storedDocuments the backend issuer CDE treatment for stored PAN.
Primary Account Number (PAN) is masked when displayedDocuments default masked PAN display and customer-requested card-detail reveal behavior.
Payment page tamper detectionRecords the position for payment-page tamper-detection evidence and absence of Green-Got-operated payment gateway pages requiring PAN entry.

Scope Conclusion

The Green-Got web application shell and mobile application shell are excluded from the CDE for iframe-handled card display, PIN display, and PIN set/change flows because they do not store, process, transmit, render, log, or modify clear PAN, CVC, PIN, or PIN-change values. The CDE-hosted iframe performs the sensitive data collection and rendering, and that iframe remains inside Green-Got’s PCI DSS Level 1 scope.

The web and mobile shells remain security-impacting software. They are handled through Green-Got’s PCI DSS secure software, authentication, change management, monitoring, and vulnerability management controls rather than through a separate mobile-application PCI certification.

Incident Response plan is reviewed and tested at least annually and individuals with IR responsibilities are trained ❌

Provide evidence of annual incident response plan testing. Guidance: Provide evidence that review and training activities related to incident response are done at least annually (screenshots or reports of tabletop results, IT Team meeting minutes. etc.) Sample tabletop document: Google docs template / Docx template

Evidence

Incident report or root cause analysis ❌

Provide a completed incident report that demonstrates how a root cause analysis (RCA) was performed following a security incident. This report should follow a structured approach to investigating, responding to, and resolving security incidents while ensuring lessons are learned and corrective actions are taken to prevent recurrence. Here’s a sample template: Google docs template / Excel template

Screenshot or other evidence that incidents are tracked and documented using methods that support all legal, operational and regulatory requirements and are reported to the appropriate authorities when necessary Note: This commonly means that timelines are tracked and established, evidence is retained, impact is officially documented, etc. For more information please see: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf

Evidence

Green-Got documents incident handling, legal review, regulatory reporting, and authority notification requirements in the Incident Response Plan.

The plan requires reported security events, security incidents, and response activities to be documented in the incident record and supporting documentation repository. Reports received by email are recorded in the company ticket management system, incident and event tickets are monitored, and the incident response meeting agenda includes updating incident tickets and timelines, documenting indicators of compromise, documenting root cause analysis, and planning long-term mitigations.

The plan also defines legal and regulatory handling:

AreaDocumented handling
Legal and executive reviewLegal staff and the CEO determine whether breach reporting or external communications are required. Legal and executive staff also determine immediate and long-term mitigation or remedial actions.
External breach reportingBreaches are reported to customers, consumers, data subjects, and regulators without undue delay and in accordance with contractual commitments and applicable legislation.
External communications approvalIncident or breach information is not disclosed to external parties or unauthorized persons without approval from legal or executive management.
Regulatory cooperationGreen-Got cooperates with customers, Data Controllers, and regulators to fulfill incident and data breach obligations.
CNIL notificationThe plan defines when CNIL notification is required, the 72-hour notification timeline, the two-step notification process, required notification content, and breach recording requirements.
ACPR notificationThe plan defines criteria for major operational or security incident reporting to ACPR and the initial, interim, and final report timelines.
Law enforcement notificationThe plan defines cybercrime reporting contacts, complaint timing, insurer notification, and the incident details and technical evidence included in a complaint.

The plan assigns Legal Counsel responsibility for determining legal or regulatory exposure, determining reportable breach status with the CEO and executive management, and reviewing and approving external breach notices in writing before they are sent.

Incident response informs leadership as necessary

Screenshot or other evidence that incidents are communicated to leadership as necessary

Evidence

We have a #incidents channel where we communicate incidents. Fabien Huet a member of leadership is part of this channel to stay informed about incidents.

Channel

Members

Incident response training is appropriate ❌

Evidence that members of the organization designated as incident responders have received appropriate training directly related to their level of involvement in Incident Response activities. Note: An end user may only need to be trained in how to report an incident while a technical responder likely needs formal DFIR (Digital Forensics & Incident Response) training, prior work experience or certification Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% for personnel defined as points of contact for incident response, as randomly selected and requested by your auditor.

Incidents population ❌

List of all incidents and critical security incidents reported during the review period (critical security incidents should include all breaches, unauthorized disclosures of personal information, and that may have caused service/business operation disruptions)

Evidence

Infra changes

List of all network infrastructure related changes which include changes to network connections, new or updated network security controls, etc., that have occurred within the CDE or in-scope environment during the review period

Evidence

The infrastructure change population for the current review period is maintained in 2026.md.

Insider threat training

Provide evidence that security awareness training provided to employees covers Insider Threat Awareness. Guidance: This training should promote the reporting of suspicious activities, guidance on identifying common indicators of insider threat activity, and the correct communication channels to report suspicious activity.

Evidence

Green-Got assigns Vanta’s built-in Insider threat training to all personnel through the Employees group. This training covers:

Training is delivered annually. Individual completion is tracked in Vanta People. Training content details are listed in Vanta’s training video access reference.

Internal vulnerability scan ❌

Provide internal vulnerability scan reports that were conducted immediately following significant changes to the system or environment. These reports demonstrate that the organization evaluates the security impact of changes and remediates vulnerabilities before systems are fully promoted to production or reconnected to the cardholder data environment.

Evidence

Interview Prep

Provide the names and job titles of designated employees that own or manage controls in your PCI environment. Please also list which controls they own or manage. Your QSA will interview them on the listed topics and may ask to observe the process they support in real time.

Evidence

Control ownership at Green-Got is tracked in Vanta. Each PCI DSS test in Vanta has a designated owner — the employee responsible for managing and evidencing that control. The owner’s name and role are visible on each individual test within the Vanta platform.

The QSA is invited to review control ownership directly in Vanta, where each test lists:

This ensures the mapping between employees and the controls they own remains accurate and up to date, as Vanta serves as the single source of truth for control assignments.

Intrusion detection system alerts

Provide evidence that your Intrusion Detection Systems (IDS) and/or Intrusion Prevention Systems (IPS) are set up to detect and alert when unusual or potentially harmful activity occurs on your network or systems. Acceptable evidence can include:

Evidence

Green-Got uses AWS GuardDuty as the IDS/IPS-aligned detection and alerting control for AWS workloads in the PCI environment. The submitted Intrusion detection system installation evidence documents that GuardDuty is enabled, receives VPC Flow Log data, DNS query logs, AWS CloudTrail events, and runtime telemetry for EC2 instances hosting ECS workloads.

GuardDuty generates findings for unusual or potentially harmful activity, including suspicious network activity, command-and-control communication, DNS-based exfiltration, runtime threats, and malware indicators. Findings are routed through Amazon EventBridge to an SNS topic that delivers email alerts to the security team for review.

The current IDS/IPS alerting path is:

StepControl
DetectionGuardDuty analyzes AWS foundational data sources and Runtime Monitoring telemetry.
Finding generationGuardDuty creates findings for suspicious or malicious activity.
RoutingAmazon EventBridge receives GuardDuty findings.
NotificationSNS delivers email alerts to the security team.
ReviewThe security team triages findings through the incident response process.

Supporting evidence:

Example GuardDuty findings view:

GuardDuty findings

Operational alerting screenshots:

Current alerts Alert configuration DevOps Slack alert channel

Intrusion detection system installation

Screenshots or configuration showing that an Intrusion Detection System (IDS) is set up to monitor traffic for abnormal activity.

Evidence

AWS GuardDuty is deployed as the intrusion detection and prevention mechanism for the Cardholder Data Environment. GuardDuty is enabled across all relevant AWS regions.

GuardDuty consumes VPC Flow Log data, DNS query logs, and CloudTrail events as foundational data sources. GuardDuty accesses VPC Flow Logs through an independent and duplicate stream provided directly by the VPC Flow Logs feature — it does not manage or require separately configured flow logs in the account. This means VPC Flow Logs consumed by GuardDuty do not appear in the customer’s own VPC Flow Logs configuration (AWS GuardDuty FAQs).

In addition, Runtime Monitoring is enabled for EC2 instances hosting ECS workloads, with the GuardDuty agent automatically managed via AWS Systems Manager (SSM). This provides deeper visibility into system-level and container-level activity beyond what foundational data sources cover.

GuardDuty generates findings for anomalous or potentially malicious behavior, which are routed via Amazon EventBridge to an SNS topic that delivers email alerts to the security team for review.

Key and certificate inventory maintained

Please provide evidence that the organization maintains an up to date inventory of all keys and certificates used to access or managed production assets. This can be a combination of tools like AWS KMS, LetsEncrypt, and many others.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-06-08
Document OwnerPlatform / Security
Review FrequencyAnnual or after a material cryptographic inventory change

Green-Got maintains its key and certificate inventory through the environment parameter schema, AWS KMS, AWS Systems Manager Parameter Store, and AWS Payment Cryptography. The PAN lookup HMAC secrets are inventoried as protected active and secondary Parameter Store SecureString values that are loaded into the application at startup.

Production and non-production PAN lookup HMAC secrets are maintained as separate secrets in separate AWS accounts. The same PAN lookup HMAC key material is not reused across production and non-production environments.

pub struct Parameters {
    // ...

    pub cert_pk: Secret,
    pub cert_pk_passphrase: Secret,
    pub instant_payment_mtls_certificate: Secret,
    pub instant_payment_key_id: Secret,
    pub instant_payment_client_id: Secret,
    pub instant_payment_ca_cert: Secret,

    pub ebics_user_a005_cert: Secret,
    pub ebics_user_a005_private_key: Secret,
    pub ebics_user_a005_public_key: Secret,
    pub ebics_user_e002_cert: Secret,
    pub ebics_user_e002_private_key: Secret,
    pub ebics_user_e002_public_key: Secret,
    pub ebics_user_x002_cert: Secret,
    pub ebics_user_x002_private_key: Secret,
    pub ebics_user_x002_public_key: Secret,
    pub ebics_bank_e002_cert: Secret,
    pub ebics_bank_e002_public_key: Secret,
    pub ebics_bank_x002_cert: Secret,
    pub ebics_bank_x002_public_key: Secret,

    // ...

    pub pgp_public_key: Secret,
    pub exceet_pgp_public_key: Secret,
    pub exceet_pgp_our_public_key: Secret,
    pub exceet_pgp_our_private_key: Secret,

    pub imkac_tr31_dki_f1: Secret,
    pub imkidn_tr31_dki_f1: Secret,
    pub imksmc_tr31_dki_f1: Secret,
    pub imksmi_tr31_dki_f1: Secret,
    pub kcvv_tr31_idx1: Secret,
    pub kpvv_tr31_idx1: Secret,
    pub apata_zmk_arn: Text,
    pub apata_export_kaav_tr31: Secret,
    pub apata_export_kaav_key_arn: Text,
    pub exceet_pek_tr31: Secret,
    pub mastercard_pek_tr31: Secret,
    pub mastercard_pek_key_arn: Text,

    // ...

    pub sftp_ssh_private_key: Secret,
    pub sftp_ssh_public_key: Secret,
    pub sftp_server_public_key: Secret,
    pub exceet_sftp_ssh_private_key: Secret,
    pub exceet_sftp_ssh_public_key: Secret,
    pub exceet_sftp_server_public_key: Secret,

    // ...

    pub mastercard_simulator_ssh_public_key: Secret,
    pub mastercard_simulator_ssh_private_key: Secret,
    pub mastercard_mtls_server_ca_bundle: Secret,
    pub mastercard_mtls_client_private_key: Secret,
    pub mastercard_mtls_client_cert_chain: Secret,
    pub mastercard_sftp_ssh_private_key: Secret,
    pub mastercard_sftp_ssh_public_key: Secret,

    // ...
}

The codebase key and certificate inventory is:

Inventory groupInventory entries
Arkea instant-payment mTLS and signingcert_pk, cert_pk_passphrase, instant_payment_mtls_certificate, instant_payment_key_id, instant_payment_client_id, instant_payment_ca_cert
Arkea EBICS certificates and keysebics_user_a005_cert, ebics_user_a005_private_key, ebics_user_a005_public_key, ebics_user_e002_cert, ebics_user_e002_private_key, ebics_user_e002_public_key, ebics_user_x002_cert, ebics_user_x002_private_key, ebics_user_x002_public_key, ebics_bank_e002_cert, ebics_bank_e002_public_key, ebics_bank_x002_cert, ebics_bank_x002_public_key
Partner OpenPGPpgp_public_key, exceet_pgp_public_key, exceet_pgp_our_public_key, exceet_pgp_our_private_key
Partner SFTP and host keyssftp_ssh_private_key, sftp_ssh_public_key, sftp_server_public_key, exceet_sftp_ssh_private_key, exceet_sftp_ssh_public_key, exceet_sftp_server_public_key, mastercard_sftp_ssh_private_key, mastercard_sftp_ssh_public_key, mastercard_simulator_ssh_public_key, mastercard_simulator_ssh_private_key
Mastercard mTLSmastercard_mtls_server_ca_bundle, mastercard_mtls_client_private_key, mastercard_mtls_client_cert_chain
Payment cryptographyimkac_tr31_dki_f1, imkidn_tr31_dki_f1, imksmc_tr31_dki_f1, imksmi_tr31_dki_f1, kcvv_tr31_idx1, kpvv_tr31_idx1, apata_zmk_arn, apata_export_kaav_tr31, apata_export_kaav_key_arn, exceet_pek_tr31, mastercard_pek_tr31, mastercard_pek_key_arn

Payment cryptography key lifecycle:

Key familyInventory entriesIssuance cryptoperiodAuthorization / validation retentionRenewal process
Arkéa ZMKAWS Payment Cryptography ZMK ARN and Arkéa-distributed ZMK ARNs, including apata_zmk_arnRetained through the end of 2030 for the current production AES-256 ZMK material, per Arkéa approvalUsed only as wrapping material for importing or exchanging TR-31 blocks; not used for card authorization cryptograms. Green-Got does not transmit ZMK material to Apata.New Arkéa key ceremony before end-2030 or earlier when required by Arkéa or a cryptographic response event
TDES TR-31 issuing and payment keysimkac_tr31_dki_f1, imkidn_tr31_dki_f1, imksmc_tr31_dki_f1, imksmi_tr31_dki_f1, kcvv_tr31_idx1, kpvv_tr31_idx1, exceet_pek_tr31, mastercard_pek_tr313 years from activation for new card issuance and personalizationRetained for authorization, validation, PIN, EMV, and card-lifecycle operations for cards already issued under the key set; the 8-year retention horizon covers non-expired cards, with an additional 2-3 month rotation period granted by Arkéa during replacementArkéa draws replacement keys before the end of the issuance period; Green-Got imports the replacement TR-31 blocks into AWS Payment Cryptography and updates application key references after partner rollout
3DS AAV/CAVV keysapata_export_kaav_tr31, apata_export_kaav_key_arnPer Arkéa and Apata 3DS replacement schedulePrevious key retained only for the agreed validation overlapGreen-Got exports the replacement KAAV under the Arkéa-distributed Apata ZMK and transmits only the KAAV TR-31 block to Apata

Card-personalization PKI expiry tracking:

Certificate / PKI familyGreen-Got inventory entryCurrent expiry basisExternal renewal tracking
Mastercard card-personalization PKI for Exceet production certificatesExpiry date communicated by Arkéa and/or Exceet for the Mastercard PKI used during card personalizationMastercard PKI extension effective through the end of 2035; Green-Got does not issue cards with an expiry date later than the applicable Mastercard PKI validityArkéa, Mastercard, and Exceet operate the personalization PKI and related certificate material. Green-Got tracks the applicable PKI expiry date, reviews it around 2030 before the next payment-key cycle produces cards that would expire after 2035, and obtains updated expiry confirmation from Arkéa or Exceet after renewal.

AWS KMS:

KMS keyRegion / scopePurposeRotation / lifecycle
alias/encryption-key primary keyeu-central-1Multi-region key-encryption key for envelope encryption of sensitive application dataAWS KMS automatic rotation enabled; 30-day deletion window
alias/encryption-key replica keyeu-west-3Replica for regional continuity of the envelope encryption keyFollows multi-region KMS lifecycle; 30-day deletion window
dbKmsKeyeu-central-1Aurora/RDS database volume encryptionAWS KMS automatic rotation enabled
dbKmsKeyeu-west-3Aurora/RDS database volume encryption for the secondary production regionAWS KMS automatic rotation enabled
Route 53 DNSSEC signing keyus-east-1DNSSEC key-signing key for the public hosted zoneRoute 53 DNSSEC and AWS KMS lifecycle; 7-day deletion window

AWS certificates:

CertificateRegion / scopePurposeRenewal
Regional ACM certificatesProduction regionsPublic TLS for regional HTTPS endpoints and load balancersACM DNS validation and managed renewal
CloudFront ACM certificateus-east-1Public TLS for CloudFrontACM DNS validation and managed renewal

AWS Parameter Store:

ParameterPurpose
Production PAN lookup HMAC active SecureString valueActive 256-bit PAN lookup HMAC-SHA256 key loaded by the application at startup; 12-month cryptoperiod
Production PAN lookup HMAC secondary SecureString valueSecondary 256-bit PAN lookup HMAC-SHA256 key loaded during controlled rotation for timestamp-based dual-slot backfill; retained only within the 30-day rotation window
Non-production PAN lookup HMAC active SecureString valueActive 256-bit non-production PAN lookup HMAC-SHA256 key loaded by the application at startup; separate from production
Non-production PAN lookup HMAC secondary SecureString valueSecondary 256-bit non-production PAN lookup HMAC-SHA256 key loaded during rotation testing and validation; separate from production
Environment secret values for the codebase inventory aboveSecret backing values for Arkea, Exceet, and Mastercard key and certificate entries
tls_certLet’s Encrypt internal service TLS certificate chain
tls_keyLet’s Encrypt internal service TLS private key

Change History

VersionDateAuthorChanges
1.02026-04-20enrico@green-got.comDocument key and certificate inventory evidence for production assets.
1.12026-04-30julian@green-got.comReplace AWS KMS HMAC inventory references with active and secondary AWS Systems Manager Parameter Store PAN lookup HMAC secrets and add document control metadata.
1.22026-05-01julian@green-got.comClarify PAN lookup HMAC secondary secret usage during timestamp-based dual-slot backfill.
1.32026-05-11julian@green-got.comAlign the inventory with the unprefixed Arkea parameter schema, replace legacy KAAV references with the Apata ZMK and exported KAAV material, and add the Mastercard PEK TR-31 material and imported key ARN to the payment cryptography inventory group.
1.42026-06-04julian@green-got.comAdd payment cryptography key cryptoperiods and Mastercard card-personalization PKI expiry tracking.
1.52026-06-08julian@green-got.comRemove a nonexistent payment cryptography key, clarify the Arkéa-distributed Apata ZMK and KAAV TR-31 transmission boundary, and document the Arkéa-granted TR-31 rotation period.

Key custodians are limited and aware of their responsibilities

Evidence

We use AWS KMS for encrypting and decrypting CHD. We do not have access to AWS KMS key material. AWS documents KMS data protection controls in the AWS KMS Developer Guide. KMS decryption operations are only performed by our production servers. Individual users do not have access to KMS decryption. There are no key custodians.

Key custodians are required to formally acknowledge that they understand and accept their responsibilities

Evidence that key custodians acknowledge their responsibilities annually. Guidance: Key Custodians (individuals who manage or access keys used to encrypt and decrypt CHD or with access to key management systems) must formally acknowledge that they understand their responsibilities for protecting keys at least annually. Have each user complete the form provided as ‘Key Custodian Agreement’ at least annually, and archive form in a secure location. If keys are not managed by your organization, or individuals do not have access to keys or key management systems used to protected stored cardholder data, use the ‘Deactivate’ button and include an explanation. Google docs template / Docx template.

Evidence

Green-Got uses AWS KMS and AWS Payment Cryptography for managed cryptographic key operations, as documented in the Cryptography Policy.

AWS KMS does not expose key material to Green-Got personnel. AWS documents these KMS data protection controls in the AWS KMS Developer Guide.

KMS decryption operations are performed by production services through assigned AWS permissions. Individual users do not access KMS key material and do not perform manual key-custodian duties for keys used to protect CHD. Because Green-Got does not have personnel acting as key custodians for these managed keys, annual key custodian acknowledgements are not applicable.

Key-Encrypting Keys

Provide evidence that key-encrypting keys (KEKs) used to protect data-encrypting keys (DEKs) are:

  1. At least as strong as the DEKs they protect, and
  2. Stored separately from the DEKs to prevent unauthorized access and compromise.

Evidence

KEK Strength

Green-Got uses AWS KMS multi-region symmetric keys as the key-encrypting key (KEK). The implementation uses:

The KEK (AES-256) is cryptographically as strong as the DEKs (256-bit ChaCha20-Poly1305) it protects — both provide 256-bit security strength.

Separation of KEK and DEK

Green-Got implements envelope encryption, which enforces strict physical and logical separation between the KEK and DEKs:

KEK Storage (AWS KMS - Not Accessible)

DEK Storage (Database - KMS-Encrypted Form)

The application uses envelope encryption with the following flow:

  1. Encryption: The application performs envelope encryption

    • Requests a KMS-generated 256-bit data key from AWS KMS via GenerateDataKey
    • KMS returns: (plaintext_key, encrypted_key_blob)
    • Application encrypts payload locally using the plaintext_key with ChaCha20-Poly1305
    • Plaintext key is immediately zeroized (cleared from memory)
    • Only the encrypted_key_blob is stored in the database alongside the ciphertext
  2. Storage Format: [encrypted_key_len (4 bytes)][encrypted_data_key][ciphertext]

    • The encrypted data key is a KMS-protected blob (protected by the KEK in AWS KMS)
    • The plaintext DEK never leaves application memory and is never persisted
    • Database fields containing PAN, PIN, CVC, and other sensitive values store only the envelope-encrypted form
  3. Decryption: The application performs envelope decryption

    • Extracts the encrypted_data_key from storage
    • Sends it to AWS KMS for decryption (which requires the KEK from HSM)
    • KMS returns the plaintext data key (only during active decryption)
    • Application decrypts the payload locally, then zeroizes the plaintext key

Key Separation Benefits

Summary

Green-Got satisfies both PCI DSS requirements:

  1. KEK Strength: AES-256 KEK is at least as strong as the 256-bit ChaCha20-Poly1305 DEKs it protects
  2. Separation: KEK is stored in AWS KMS (inaccessible to the application), while DEKs are stored encrypted in the database, accessible only during active encryption/decryption operations via AWS KMS

List of all authorized VPN users

VPN authorized users access list

Evidence

Every employee of Green-Got has the ability to create an account for our VPN (Tailscale). A list of every employee is available in Employees. A list of everyone already in Tailscale here VPN Users

Log Management - Cardholder data access 📬

Accesses to cardholder data For each of the following systems, please provide one audit log sample to show that all individual accesses to cardholder data is logged. Data repositories where users have access to full PAN: Pending inventory with total system population Note: Each log should have the following details:

Log Management -General log content 📬

Audit log events For each of the following systems, please provide one audit log sample to show that each of the events below are captured: Systems: At least one of each in scope system type - To be identified by QSA. Events:

Log data protected from modification or deletion

Provide screenshots or other evidence that user permissions to modify or delete logs are tightly controlled and limited based on the principle of least privilege. Guidance: This is one of the most sensitive permissions in the organization and should be closely maintained and limited to the least number of individuals required. Typically, leveraging IAM roles in code for auditability and traceability is the best practice and there should always exist a documented business justification for wielding this permission based on their role in the organization.

Evidence

Green-Got centralizes audit and infrastructure logs in ClickHouse. The Monitoring - Log Forwarding evidence documents that audit logs are forwarded into ClickHouse and that ClickHouse Cloud performs managed backups of the log store.

ClickHouse Cloud organization-level privileges are tightly limited. The evidence below documents that only one user has the Admin role and that the remaining users are assigned the Member role.

ClickHouse Cloud roles

The Logs are retained for at least 12 months evidence documents that no expiration time or TTL is configured on the central audit log store. This protects audit log availability by preventing scheduled deletion from the central log store.

Permissions that modify or delete logs are managed under the Access Control Policy. Green-Got grants production and privileged access based on least privilege, explicit approval, MFA, time-bound use where applicable, activity logging, and quarterly access reviews.

Green-Got also applies fine-grained internal permissions based on necessary access. These permissions never grant users the ability to modify or delete audit entries. The application writes audit entries through append-only paths and does not receive permissions to modify or delete existing audit entries.

Log systems alert on error or loss of coverage ⏱️

Provide screenshots or other evidence that logging systems have a mechanism to alert administrators if they suffer an error or lose availability.

Guidance

If your main logging or SIEM tool’s service is disrupted or unavailable, there are multiple ways to monitor availability:

  1. Enable Heartbeat alerting: Send periodic “heartbeat” events in your SIEM or logging tool.

  2. Use external monitoring systems: Monitor your SIEM or logging tool via other infrastructure monitoring tool directly and enable alerts via SMS, email, and other communication platforms when they detect downtime. Regularly monitor server health directly.

  3. Develop a redundant scheme: Set up multiple nodes (if possible) and develop disaster recovery and failover plans in the event your centralized SIEM or logging tool goes down.

  4. Leverage cloud monitoring: If you’re running a SIEM or logging tool on a cloud platform such as AWS, GCP, or Azure — leverage their native monitoring and alerting tools (e.g., CloudWatch) to monitor the health of any instances as applicable.

Audit consideration: During your audit, your assessor may request you to re-upload evidence for at least 10% of logging systems to confirm error logging is enabled for relevant in-scope systems that they analyze populations for.

Evidence

We are already using 1. The nature of metrics are that they are send in short intervals anyways also we never have no logs or traces being send at any moment. If we detect that sending telemetry is impaired our application informs us via Slack about it. (so 2.) We are currenly always logging both to our central logging solution and also AWS Cloudwatch as a fallback for the particular case described here.

Logging and Incident Response support is available to customers on demand

Provide evidence that you make appropriate logging to support incident response available to all customers and support their incident response processes if/when necessary including:

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.2 and A1.2.1 apply to multi-tenant service providers that must make per-customer logging available and support customer-specific incident response activities. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not expose separate customer log environments.

Green-Got maintains logging and incident response processes for its own PCI DSS environment. Those controls are documented in the internal logging and incident response evidence set.

Logging and alerting is configured on PCI-impacting applications and systems ⏱️

Configure logging and alerting for PCI systems and applications. Guidance: The DSS requires logging and alerting to be configured on PCI-impacting system that captures important events that could aid in the execution of a forensics investigation. Audit trails must include:

Evidence

Logging and alerting is configured across PCI-impacting systems using the following process:

Log Collection

Application and infrastructure logs (AWS CloudTrail, PostgreSQL) are collected and stored in ClickHouse via the ClickStack pipeline. This captures key events including privilege escalation, authentication attempts, database errors, and infrastructure changes.

Alerting Configuration

Logs are exposed as sources in HyperDX (ClickStack), where alerts are configured based on patterns relevant to security and availability. Examples of configured alerts include (non-exhaustive):

Alert Routing & Review

All alerts are delivered to a dedicated Slack channel via webhook. Alerts are reviewed by the team on a regular basis. Critical alerts trigger immediate investigation, while medium/high severity alerts are reviewed as part of operational checks.

Screenshots

Alerts Overview Alerts Overview 2

Logs are immediately available online for at least three months

Evidence that three months of logs are kept online and available for immediate analysis. Guidance: Provide screenshot of logging platform showing at least three months are available online. If logs are retained online for one year the same evidence can be used to demonstrate that this control is being met.

Evidence

Green-Got uses Signoz for online log analysis over the most recent three months. The evidence below documents the configured online analysis range without exposing individual log entries.

Signoz production log timeline over a 16-week range

Green-Got retains audit logs in ClickHouse as the central online audit log store. The Monitoring - Log Forwarding evidence documents that audit logs are forwarded into ClickHouse and that ClickHouse Cloud performs managed backups of the log store.

The evidence below documents online ClickHouse log availability using a daily aggregate query for the production cloudtrail_events table without displaying individual event records.

ClickHouse daily aggregate chart for cloudtrail_events

The Logs are retained for at least 12 months evidence documents that no expiration time or TTL is configured on the central audit log store. Audit log records therefore remain available online in ClickHouse beyond the three-month immediate-availability requirement.

This retention posture is subject to change. Green-Got may reduce audit log retention in the future, but will maintain audit logs for at least 12 months, including at least three months immediately available online for analysis.

Logs are retained for at least 12 months

Provide evidence that audit logs are retained for at least 12 months.

Evidence

Green-Got retains audit logs in ClickHouse as the central long-term audit log store. The Monitoring - Log Forwarding evidence documents that audit logs are forwarded into ClickHouse and that ClickHouse Cloud performs daily managed backups of the log store.

Green-Got has not configured an expiration time or TTL on the central audit log store. Audit log records therefore remain available in ClickHouse as they age, including beyond the 12-month retention requirement.

This indefinite retention posture is subject to change. Green-Got may reduce audit log retention in the future, but will maintain audit logs for at least 12 months.

MFA enabled for in-scope systems

Provide screenshots of your multi-factor solution’s login window across your in-scope systems for 3 users (or all if fewer than 3) from 3 different teams (or all if fewer than 3). For example, while attempting to connect, showing the GUI with the different factors required as part of the multi-factor authentication process.

Evidence

The Green-Got Back Office uses multi-factor authentication.

Back Office access is gated by two independent authentication factors:

@chloe

An operator reaches the Back Office only after authenticating to Tailscale and completes Back Office sign-in only after successfully responding to the WebAuthn challenge with a registered passkey. The Back Office does not use username-and-password authentication.

AWS human access is performed through AWS IAM Identity Center.

The MFA control for AWS human access is configured in AWS IAM Identity Center. The Pulumi configuration in this repository does not manage IAM Identity Center MFA settings.

The screenshot below shows the AWS IAM Identity Center MFA configuration and evidences that multi-factor authentication is enabled for all AWS human access.

AWS IAM Identity Center MFA enabled

The following screenshots document the AWS IAM Identity Center sign-in flow, showing the two authentication factors required for access:

Step 1 — Username: The user identifies themselves with their username.

AWS sign-in — username

Step 2 — Password (first factor): The user enters their password.

AWS sign-in — password

Step 3 — MFA code (second factor): After entering their password, the user is prompted for additional verification. The user enters the six-digit code from their authenticator app.

AWS sign-in — MFA code

Step 4 — Access granted: After completing both authentication factors, the user reaches the AWS access portal.

AWS access portal

AWS references:

For the remaining in-scope third-party services, sign-in is performed through Google OAuth where the vendor supports federated authentication. The supporting screenshots and vendor-specific evidence are maintained in the Vanta vendor inventory used for PCI evidence collection.

MFA is securely configured

Provide vendor documentation, configuration settings or other evidence that: • The MFA system is not susceptible to replay attacks. • MFA systems cannot be bypassed by any users, including administrative users unless specifically documented, and authorized by management on an exception basis, for a limited time period. • At least two different types of authentication factors are used. • Success of all authentication factors is required before access is granted.

Evidence

Green-Got enforces MFA for AWS human access through AWS IAM Identity Center.

AWS IAM Identity Center requires users to authenticate with their username and password and complete the configured MFA step before access is granted. Green-Got maintains the AWS IAM Identity Center configuration and sign-in flow evidence in MFA enabled for in-scope systems.

AWS vendor documentation states that IAM Identity Center MFA for directory users uses two different authentication factor types:

AWS documents that users are prompted to register an MFA device and that sign-in requires successful completion of the configured MFA step before access is granted.

AWS references:

For replay resistance, AWS Identity Center supports phishing-resistant authenticators, including FIDO2 security keys and biometrics-backed passkeys.

AWS references:

For remaining in-scope third-party services, Green-Got uses Google OAuth where the vendor supports federated authentication. Vendor-specific MFA evidence is maintained in the Vanta vendor inventory used for PCI evidence collection.

Maintain a program to track PCI-impacting Service Providers' compliance status

Evidence that a process is in place to vet and periodically track Service Providers impacting the security of PCI systems and data. Guidance: Complete and upload sample Service Provider Inventory documentation.

Evidence

Green-Got maintains its PCI-impacting third-party service provider inventory in Vanta Vendor Management.

The managed vendor inventory includes the service providers that store, process, transmit, or affect the security of cardholder data and PCI systems, including:

Vanta is the source of truth for vendor status, owner assignment, inherent risk, security review state, review dates, and supporting evidence. The supporting due-diligence and risk-review process is documented in Vendor Due Diligence, Vendor Risk Assessments, and the Third-Party Management Policy.

Maintain data inventory map ❌

Provide a link or upload your most recent Data Inventory Map.

Guidance: Here’s a sample Data Inventory Map template. Google docs template / Excel template

Note: Documenting your Records of Processing Activities (ROPA) can adequately fulfill the requirements of a Data Inventory but it is highly recommended to have a holistic approach to Data Inventory Mapping through the implementation of a Data Governance Program. This can include maintaining a structured metadata repository for databases and cloud resources, and using a discovery engine to scan for unstructured data across all environments (e.g., endpoints, cloud stores, documentation repositories, SaaS apps, etc.).

Maintenance and use of only currently supported systems

Provide evidence that the organization only uses currently supported systems and has ongoing maintenance or support for all in scope applications and hardware.

Evidence

In-scope system inventory

All infrastructure is defined as code using Pulumi and stored in this repository. The following components are deployed in the cardholder data environment:

ComponentVersionVendor support status
AWS Aurora PostgreSQL17.5Supported through at least 2029, extended lifecycle support enabled
Amazon Linux 2023 (ECS hosts)AL2023Supported through 2028
Debian stable-slim (container base)TrixieActive security support from the Debian Security Team
TLS1.3 minimum (TLSv1.3_2025 CloudFront policy)Current standard per IETF RFC 8446
AWS ECSCurrent generationFully managed by AWS
AWS CloudFrontCurrent generationFully managed by AWS
AWS WAF v2Current generationFully managed by AWS
AWS GuardDutyCurrent generationFully managed by AWS
AWS KMSCurrent generationFully managed by AWS
ClickHouse CloudCurrent generationFully managed by ClickHouse Inc.

No end-of-life or unsupported software is present in the CDE.

Automated dependency maintenance

Automated processes run in production to keep dependencies current:

  1. Daily security alert remediation (fix_github_security_alerts automation) — runs every day, queries open GitHub Dependabot alerts, and creates PRs to resolve security vulnerabilities.

  2. Weekday dependency updates (update_dependencies automation) — runs Monday through Friday and creates focused PRs that advance coherent batches of dependency updates. Single-dependency PRs are reserved for security-critical updates, cases where only one safe update is available, or cases where broader batching is blocked.

Monorepo structure

All application code, infrastructure definitions, and automations live in a single repository that is actively worked on every day. There is no risk of relying on a stale or unmaintained internal codebase — every component is continuously updated as part of normal development activity.

Annual review

The system inventory and vendor support status are reviewed annually as part of the PCI DSS audit cycle. Since all infrastructure is defined as code, the source of truth for deployed versions is always the current state of the repository.

Malware configuration

Provide evidence that an anti-malware solution is configured to detect, remove, and protect systems against all known types of malicious software. This includes vendor documentation and screenshots of the anti-malware dashboard or configurations showing that threat detection, removal, and protection are enabled, with engines and definitions up to date. Ensure that periodic or real-time scans are performed, and logs are retained for one year offline and three months online.

Evidence

Green-Got manages employee workstations via Fleet MDM and Primo MDM. Workstation anti-malware is applied based on the platform assessment documented in A process is in place to determine whether certain OS types do or do not require malware protection:

The Windows and macOS solutions satisfy the requirement for an anti-malware solution that detects, removes, blocks, and contains all known types of malware on workstation platforms identified as requiring anti-malware (Requirement 5.2.2). Linux workstations are documented as excluded in the platform assessment.

Deployment and coverage (5.2.1, 5.2.1.a, 5.2.1.b)

Anti-malware protection is deployed on all workstation system components identified as at risk from malware. Linux workstations are excluded from the workstation malware scanner scope through the platform assessment. Primo enforces that every enrolled Windows workstation has Microsoft Defender installed and active via two MDM profiles:

Primo ControlOSStatus
Company default - Microsoft Defender Security ConfigurationWindows 11 ProDeployed
Windows Devices - Microsoft Defender Maximum ProtectionWindows 11 ProDeployed

Primo — Microsoft Defender Security Configuration Primo — Microsoft Defender Maximum Protection

On macOS workstations, XProtect is enabled by default as part of the operating system. Fleet osquery policies continuously verify that XProtect, the macOS firewall, and System Integrity Protection (SIP) are active on all managed Macs:

Fleet PolicyPurposeStatus
macOS - XProtect enabledVerifies XProtect is present and activeActive
macOS - Firewall enabledVerifies macOS firewall is onActive
macOS - SIP enabledVerifies System Integrity Protection is onActive

The Vanta automated test Malware detection on personnel workstations (Primo) continuously verifies that antivirus software is present on in-scope personnel workstations. This test currently passes with zero failing items.

Server-side infrastructure runs on AWS and uses Amazon Linux-based container images managed through ECS. EC2 hosts are rotated frequently through automated deployments, and application containers run with read-only filesystems. Amazon Inspector provides vulnerability scanning for EC2 hosts and container images. AWS GuardDuty Runtime Monitoring is enabled for EC2 instances hosting ECS workloads and provides runtime threat detection through the GuardDuty security agent. GuardDuty Malware Protection for EC2 is enabled as the AWS-native malware scanning control for EC2 and ECS-on-EC2 workloads, scanning EBS volumes attached to EC2 instances and container workloads running on EC2.

Automatic updates (5.3.1, 5.3.1.a, 5.3.1.b)

Anti-malware solutions on both platforms receive automatic updates:

No manual intervention is required to keep anti-malware signatures and engines current.

Scanning and behavioral analysis (5.3.2, 5.3.2.a, 5.3.2.b, 5.3.2.c, 5.3.2.1.b)

The deployed anti-malware solutions perform both periodic scans and continuous real-time protection:

Scan results and detections are logged in each solution’s management console and are available for review.

Removable electronic media (5.3.3, 5.3.3.a, 5.3.3.b, 5.3.3.c)

On Windows, Microsoft Defender is configured to perform automatic scans when removable electronic media (USB drives, external storage) is inserted, connected, or logically mounted. On macOS, XProtect scans files from removable media when they are opened or executed.

Audit log retention (5.3.4)

Audit logs for anti-malware solutions on both platforms are enabled:

Log retention is maintained in accordance with Requirement 10.5.1 (at least 12 months total, with a minimum of three months immediately available for analysis).

Tamper protection (5.3.5, 5.3.5.a)

Anti-malware mechanisms are not alterable or disableable by end users:

Disabling anti-malware protection is not permitted under any circumstances.

See also Antimalware tamper protections for additional evidence.

Security controls on devices connecting to untrusted networks and the CDE (1.5.1.b)

Computing devices that connect to both untrusted networks (including the Internet) and the CDE have security controls actively running and configured to prevent threats from being introduced into the network. Primo and Fleet enforce specific configuration settings including active anti-malware on in-scope workstation platforms (Microsoft Defender on Windows, XProtect on macOS), hard disk encryption on managed Windows and macOS workstations (BitLocker on Windows, FileVault 2 on macOS), and firewall settings on managed workstations. Linux workstations are excluded from the workstation malware scanner scope through the platform assessment. These controls are not alterable by users unless specifically documented and authorized by management.

Security alerting

All Fleet policy failures automatically trigger an email alert sent to security@green-got.com. Alerts are delivered in real time via a Google Apps Script webhook connected to Fleet MDM automations.

Alert typeTriggerDestinationTool
Policy failureAny host fails a Fleet security policysecurity@green-got.comFleet + Google Apps Script
Host offlineA device stops checking into Fleetsecurity@green-got.comFleet host status webhook

Uploaded evidence

The Endpoint Security Evidence PDF contains detailed per-platform breakdowns of all endpoint security controls, including MDM configuration screenshots, Fleet policy status, and a compliance summary matrix.

Malware protections deployed

Provide screenshots or documentation demonstrating that all publicly exposed or risk-assessed vulnerable systems (e.g., web servers, bastion hosts) have active anti-malware protection. The solution must support regular updates (including scan engine and definitions), scheduled system scans, and scanning of external media upon connection. If relevant tests are enabled in your Vanta account, this document is not required. Note: If any of these tests are enabled in your Vanta account, this document is not required. Tests:

Evidence

Green-Got documents anti-malware deployment and monitoring in the submitted Malware configuration evidence.

Anti-malware protection is deployed on employee workstation platforms identified as requiring anti-malware:

The submitted malware configuration evidence also documents:

Server-side infrastructure runs on AWS using ECS on Amazon Linux EC2 hosts. Green-Got rotates the EC2 host machines, while application containers run with read-only filesystems. Container images and EC2 hosts are scanned for vulnerabilities through the vulnerability management workflow, including Amazon Inspector coverage. AWS GuardDuty Runtime Monitoring is enabled for EC2 instances hosting ECS workloads and provides runtime threat detection through the GuardDuty security agent. GuardDuty Malware Protection for EC2 is enabled for EC2 and ECS-on-EC2 workloads.

Green-Got distinguishes vulnerability scanning, runtime threat detection, and malware scanning for server-side systems. Amazon Inspector is the vulnerability scanning control for EC2 hosts and container images. GuardDuty Runtime Monitoring is the runtime threat detection control for EC2-hosted ECS workloads. GuardDuty Malware Protection for EC2 is the AWS-native malware scanning control for EC2 and ECS-on-EC2 workloads, scanning EBS volumes attached to EC2 instances and container workloads running on EC2.

Media disposed of or repurposed securely

Screenshot, photo, certificate of destruction from a vendor or other evidence that physical media is securely wiped prior to being moved offsite for maintenance or disposal (if applicable)

Evidence

Green-Got does not store cardholder or customer data on physical media or endpoint devices (such as employee laptops or removable media). All sensitive data is stored exclusively within AWS-managed services.

As a result, the organization’s physical media does not contain sensitive data requiring sanitization prior to disposal or repurposing.

Therefore, secure media wiping or destruction procedures specific to cardholder data are not applicable.

Media scanned for malware

Screenshot or other evidence that technical anti-malware mechanisms and physical media use processes ensure that media used for diagnostic purposes or that contains troubleshooting applications are validated to be free of malware

Evidence

Green-Got does not use removable media. Diagnostic and troubleshooting activities are performed through managed systems and approved software distribution paths rather than removable media.

The submitted Malware configuration evidence documents anti-malware controls for managed endpoint platforms, including removable-media scanning behavior where removable media is connected to a managed endpoint.

Because Green-Got does not use removable media, no separate media-scanning sample exists for this workflow.

Messaging queue message age monitored

This test verifies that all AWS SQS queues have appropriate CloudWatch alarms configured to monitor the ApproximateAgeOfOldestMessage metric, which indicates message processing delays or potential queue blockages.

Evidence

We are deactivating this Vanta test because we monitor Amazon SQS message age through SigNoz’s built-in AWS monitoring instead of maintaining separate CloudWatch alarms in Vanta.

SigNoz’s AWS integration collects Amazon SQS metrics from AWS and provides centralized monitoring for queue health, including queue age:

AWS documents that Amazon SQS publishes operational metrics to CloudWatch, including ApproximateAgeOfOldestMessage, which is the metric used to monitor queue age:

Because ApproximateAgeOfOldestMessage is already monitored centrally through SigNoz using AWS metrics, this Vanta test is duplicate coverage and is deactivated.

Provide documentation showing that your organization actively monitors industry trends to stay informed on technology status and emerging risks, including when vendors have announced EOL plans for a technology and updates related to the viability and security of cryptographic cipher suites and protocols. This ensures your technology remains compliant and secure in the face of evolving standards and vulnerabilities.

Evidence

Green-Got monitors PCI-relevant technology status, vendor support status, vulnerability intelligence, and cryptographic viability through the following documented evidence:

AreaEvidence
Technology and EOL monitoringThe Technology Review and EOL Remediation Plan documents the annual recurring review of in-scope hardware and software technologies, including vendor support and end-of-life remediation planning.
Supported system inventoryMaintenance and use of only currently supported systems documents the current in-scope infrastructure, software versions, vendor support status, automated dependency maintenance, and annual review process.
Vulnerability intelligenceVulnerability Intelligence Sources documents active monitoring through GitHub Dependabot for known vulnerabilities in software dependencies.
Cryptographic protocols and cipher suitesCipher suite and protocol inventory documents the cryptographic protocols and cipher suites currently in use, where they are used, and the annual review process.
Cryptographic vulnerability responseCryptographic response plan documents the response process for newly discovered cryptographic vulnerabilities, deprecated cipher suites, compromised keys, certificate failures, and related cryptographic failure events.

Together, these documents establish the current process for monitoring PCI industry trends that affect supported technologies, vendor lifecycle status, vulnerability exposure, and continued viability of cryptographic cipher suites and protocols.

NACL change management

Please provide at least one example of a change management ticket related to the management of a network access control list or other network security ruleset. Your QSA may request additional information or examples if they need it.

Evidence

Green-Got manages network security controls as infrastructure as code. Changes to network security rulesets are reviewed and approved through the pull-request workflow before implementation.

The example below documents a network security ruleset change, including the reviewed pull request and the network-related files changed.

Network security rule change pull request

Network security rule change files

The submitted Network & CSP - Change tickets sample - General and Network & CSP - Change tickets sample - New/updated rules evidence documents the same change-management workflow for network security controls.

Network & CSP - Change tickets sample - General 📬

Provide change control records for a sample of rule set or configuration changes applied to firewalls, routers, cloud networking tools, or other network security controls (NSCs). Each record should show that changes were formally reviewed, approved, and authorized prior to implementation. This evidence validates that network-layer changes follow secure and auditable change management practices in line with PCI DSS expectations.

Evidence

Network configurations are managed with Infrastructure are Code. They follow the same change management procedure that all code changes follow.

Pull Requst Files Changed

Once a pull request to change any NSCs is done, it has to be reviewed and approved prior to merging. There is no need to authorize NSCs changes, the person assigned to review automatically provides authorization by approving.

Network & CSP - Change tickets sample - New/updated rules 📬

Provide change control records for a sample of changes made to network infrastructure, specifically those involving new or modified network connections. Each record should demonstrate that the change was reviewed, approved, and managed in accordance with your organization’s change management procedures. These records confirm that changes were properly evaluated for security impact, and followed documented processes for authorization.

Evidence

Network configurations are managed with Infrastructure are Code. They follow the same change management procedure that all code changes follow.

Pull Requst Files Changed

Once a pull request to change any NSCs is done, it has to be reviewed and approved prior to merging. There is no need to authorize NSCs changes, the person assigned to review automatically provides authorization by approving.

Network Change Management 📬

Representative sample of network change records from last 12 months including reason, impact, owner, approver, etc.

Network access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login or access attempts to your cloud service providers network administration console or on 3 different network devices or (e.g. a switch, router, firewall) using the vendor-supplied default accounts and passwords (e.g. Cisco, Administrator, Admin, etc) The evidence must specify the default accounts/passwords used.

Evidence

Our network infrastructure is entirely cloud-based on AWS. There are no physical network devices (switches, routers, firewalls) to manage. All network components are fully managed AWS services accessed exclusively through IAM-based authentication:

None of these services provide vendor-supplied default accounts or passwords. AWS services are accessed solely through IAM credentials set by the customer, and Tailscale authenticates through an external identity provider. There are no default credentials to test against.

Network configurations centrally managed

Please provide evidence of your cloud platform and/or network management consoles user interface

Evidence

Green-Got centrally manages network configuration through Pulumi infrastructure as code in the repository.

Network resources, including VPC configuration, routing, security groups, load balancers, and related network security controls, are defined as code. Changes are reviewed through the default change management process before deployment, which provides centralized change history, approval, and version control for network configuration.

Network diagram

Network Diagram

Provide a detailed network diagram. A proper network diagram consists of several key components and attributes:

Evidence

Diagram

Boundary Definitions

Network Devices

It’s a purly virtual network so it does not have many of the devices found in physical networks.

Servers: AWS EC2 Firewall: AWS WAF and AWS Shield Bastion servers We don’t have any bastion servers. Our application servers sit behind layers of

graph TD
    Internet[Internet] --> CF[Cloudfront / Global Accelerator]
    CF --> Shield[AWS Shield / WAF]
    Shield --> ALB[AWS Application Load Balancer]
    ALB --> EC2[Application Servers]

access though the intetnet. Of we internally connect to our server this is done via Tailscale which establishes a direct connection to the server since tailscale is a Mesh VPN.

Routers: No routers, we have a route table that does the following:

Communication between subnets and VPC’s is needed so the database can successfully replicate. Route tables are not used to control dataflow, we use Security Groups for this explained later

Data Flows

Request from the public internet

graph TD
    Internet[Internet] --> CF[Cloudfront / Global Accelerator]
    CF --> Shield[AWS Shield / WAF]
    Shield --> ALB[AWS Application Load Balancer]
    ALB --> EC2[Application Servers]
    EC2 --> DB[Database]

Internal request

graph TD
    Employee[Employee] --> EC2[Application Servers]
    EC2 --> DB[Database]

Request from mastercard

graph TD
    Mastercard[Mastercard] --> ALB[AWS Application Load Balancer]
    ALB --> EC2[Application Servers]
    EC2 --> DB[Database]

Most request don’t just touch the database the might also call out to third party services via the internet or to any service we have a Private Link to like S3, Temporal Cloud, Clickhouse Cloud.

Network Zones

The differentation between public and private network zones are not needed for us. Network zones are just split since it’s a limitation by AWS so we have 3 subnets per region, so a total of 6. One per Availablity Zone.

Security Controls

Since we run a monolith a lot of security controls are embedded on an application level. Security controls we maintain on a networking levels are controlled via AWS Security Groups and Tailscale Grants.

AWS

  1. Only our Cloudfront distribution, Mastercard and our Global Accelerator are allowed to connect to our application load balancer. All of these connections happen via private networking. Our Cloudfront distribution and Global Accelerator are our only public endpoints. The Global Accelerator provides an additional entry point for TCP and UDP traffic, both exclusively on port 443. No other ports are open. Health checks use the HTTPS protocol. The Global Accelerator is used only in cases where mTLS (mutual TLS) support is required, for example communicating with Arkea.
  2. The application load balancer can only connect to our application on the provided port
  3. Our application can talk to the entire internet but can only be talked to from devices on our VPC.
  4. Only our application and our Network load balancer can talk to the database on the port of the database.
  5. The database does not have outgoing traffic.
  6. Our network load balancer can only be accessed by Clickhouse Cloud on the port of the database. (This is done to replicate the database to our data warehouse)

Tailscale

  1. CICD, admins and developers can connect to any port of the application and can use the application as a Subnet router to connect the the entire VPC and Tailscale Apps to connect to third party services using our static IP range

  2. Everyone else, just access to 443 to make normal HTTPS calls internal services

Critical Systems

We run a monolith so all component’s outlined are critial components.

Physical Locations

Are are running in eu-central one as our primary location and eu-west-3 as a fallback region. Some AWS services require to run in us-east-1 like certificate creation for Cloudfront, so they are run there.

Redundancy and Failover

In case eu-central-1 is down we do a switchover at the level of our AWS Global Accelerator and AWS Cloudfront, we will just update the region they are pointing to. Both AWS Global Accelerator and AWS Cloudfront are globally deployed AWS services.

We use Aurora Global database as our primary datastorage which is replicated to our fallback region and can take over in matter of seconds.

Network security controls capabilities documentation

Provide vendor documentation for your network security controls solution that explains its capabilities for blocking, detection, alerting, etc.

Evidence

Security controls we maintain on a networking levels are controlled via AWS Security Groups and Tailscale Grants.

Network segregation

Provide evidence demonstrating that the production network is segregated from other environments (e.g., development, testing, corporate networks). Acceptable evidence can include:

Evidence

Green-Got segregates production from non-production environments using separate AWS accounts, separate Pulumi stacks, separate VPCs, independent IAM policies and credentials, independent database clusters, and independent network security controls.

The Compute environments segregated evidence documents this separation, including the absence of network peering or cross-account access between production and staging environments.

The Network diagram evidence documents the production CDE network boundaries, ingress paths, internal traffic paths, database restrictions, and Tailscale Grants used for administrative access.

The Non-production environment access is different from production access evidence documents separation of production and non-production access paths.

Network services have business justifications

Provide an example of a business justification or other documented criteria used to determine why ports and network services are allowed or disallowed. This is especially applicable in scenarios where commonly insecure protocols or configurations are in use.

Evidence

Green-Got documents approved network services, protocols, ports, and business justifications in the submitted Established configuration standard for firewalls and routers evidence.

The approved services table identifies each allowed service, its protocol and port, and the business justification for allowing it. The same evidence documents that insecure public administrative services are not approved entry points in the production environment.

Non-consumer password settings are configured and managed securely

This requirement applies only to Service Providers who control password settings for non-consumer users, defined as: “Individuals, excluding cardholders, who access system components, including but not limited to employees, administrators, and third parties.” Provide screenshots or exports from your authentication systems (e.g., identity providers, local OS, cloud platforms, or VPN) demonstrating that non-consumer user accounts are configured as follows:

Evidence

This requirement does not apply to Green-Got. PCI DSS 8.3.10.1 governs password settings for “customer user access,” targeting service providers that manage authentication for other businesses’ non-consumer users (employees, administrators, third parties). Green-Got does not provide payment processing services to other companies and does not manage password settings for any external organization’s non-consumer users as a result. Green-Got’s customers are consumers accessing their own payment card information, which is explicitly excluded from Requirement 8.3.10.1. General password controls (8.3.4, 8.3.6, 8.3.7) for Green-Got’s own internal non-consumer users (employees and administrators) are addressed in the standard password and authentication requirements applicable to all entities.

Non-privileged user lists generated

Provide a screenshot or system export showing the non-privileged users for all in-scope systems and applications. You should consider users of all types, including business partners, service accounts, vendor and third party accounts, contractors, and employees. Guidance: Ensure that the standard access configured for your in-scope systems is based on active and current roles & responsibilities of those assigned individuals. If there are newly added individuals, ensure that you followed your access provisioning process to create / modify the accounts, and removed any personnel that did not require the access anymore. Guidance: Typical in-scope systems include:

  1. Background checkers
  2. Cloud providers
  3. Communication platforms
  4. CRM platforms
  5. Database/Data warehouse providers
  6. Endpoint security tools
  7. HRIS
  8. Identity providers
  9. MDM tools
  10. Vulnerability scanners
  11. SIEM tools
  12. Version control systems
  13. DevOps tools
  14. Document repositories Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% of your standard user accounts, as randomly selected and requested by your auditor. Note: This evidence is only required for non-admin users of tools that have not been connected to Vanta via the integrations page. For tools that have been integrated into Vanta, no additional evidence is needed for this document.

Evidence

Green-Got uses Vanta Access as the authoritative inventory for active personnel and their access assignments. Non-privileged access is the baseline access level for Green-Got employees.

For systems integrated with Vanta, Vanta Access provides the personnel population and the systems each person has access to.

Green-Got’s internal back office is not directly integrated with Vanta. All active employees receive baseline non-privileged access to the back office. Privileged back-office access is controlled separately and is not part of the baseline non-privileged population.

As a result, the non-privileged user population for the back office aligns to the active employee population maintained in Vanta.

Non-production environment access is different from production access

Provide screenshots of access lists to your in-scope system components showing that development/test environments have different access accounts/usernames than production environments. Guidance: Having separate accounts in each environment outlines segregation of duties and role-based access control across your development lifecycle. It also ensures that not all personnel can access/make modifications to your production environment.

Evidance

Internal systems

Internal systems are hosted on different URL’s. Non production versions are visually seperated

Internal Systems

Back Office

AWS

AWS

Temporal

For Temporal accounts are not seperated but there is a clear seperation between environement’s both visually and form a access management perspective

Temporal Temporal Access Management

Office / Facilities

Green-Got does not operate any company-managed office facilities or physical locations within the scope of the Cardholder Data Environment (CDE). The organization follows a remote-first model, with employees optionally using third-party coworking spaces that are not managed or controlled by Green-Got.

All production systems and data are hosted within AWS, and no physical access to coworking locations or employee environments provides direct access to the CDE. Access to production systems is strictly controlled through authenticated, encrypted connections via Tailscale using WireGuard, independent of user location.

Employee devices are secured through standard endpoint protections (e.g., disk encryption, access controls), and customer or cardholder data is not stored locally on these devices.

As a result, there are no physical facilities within scope that require traditional physical security controls for the CDE.

Office / facility visitor access restricted

Screenshot or other records showing that visitors and third parties such as vendors that are visiting your in-scope locations (such as offices, facilities, secure areas, etc) are granted access to areas based on least privilege. You much also show that access is revoked or expires automatically when no longer needed (where applicable), including any physical/logical access mechanisms they were provided (such as badges, keys, etc). Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% of visitors to your in-scope facilities, as randomly selected and requested by your auditor.

Evidence

See Office / Facilities

Visitor log

Provide all in-scope office visitor logs from the last 30 days. Note: If you don’t have a physical office, remove this document request from scope.

Evidence

See Office / Facilities

Visitors are escorted

Evidence that visitors and third parties such as vendors are supervised or escorted at all times while on site (if applicable)

Evidence

See Office / Facilities

Visitors are monitored and identifiable

Evidence that visitor badges are distinct from employee badges or that visitor activity is otherwise monitored via escorting and access denial to sensitive areas, etc. Guidance: Picture of visitor pass/badge next to employee badge. Redaction of PII or other sensitive information is fine, just make sure not to redact so much that the two can’t be told apart. Audit Consideration: Your assessor may come onsite to your facilities to validate samples of visitors and employees to confirm your processes are working as expected.

Evidence

See Office / Facilities

Only secure methods and protocols are used for the transfer and administration of PCI systems

Evidence of running services demonstrating no insecure protocols are in use.

Guidance: Upload internal and external scan results demonstrating that no weak methods or protocols are used for system administration (e.g. telnet, RDP, or HTTP).

Evidence

Remote access to internal systems

All remote access to internal systems goes through Tailscale, which is built on WireGuard, a modern VPN protocol using strong cryptography (ChaCha20, Poly1305, Curve25519, BLAKE2s). No internal service is exposed to the public internet; access is only possible through the Tailscale network. All traffic between administrators and internal services is encrypted in transit via the WireGuard tunnel, regardless of the underlying application protocol. This means that even if a service uses an unencrypted protocol internally, the communication remains protected by the WireGuard encryption layer.

Internal VPC communication

Communication between services within our AWS VPC is controlled through security groups and network ACLs that enforce least-privilege access between components. Only explicitly authorized traffic flows are permitted; all other traffic is dropped at the network level.

AWS qualifies a correctly designed VPC as a private network under PCI DSS, with less stringent encryption requirements than public networks. The underlying AWS infrastructure provides additional protections that make traffic interception within the VPC not feasible:

These protections apply to all traffic within the VPC, including communication between the Application Load Balancer and EC2 targets, and between the application and the database. AWS explicitly confirms that traffic between the load balancer and its targets within a VPC is authenticated at the packet level and is not at risk of man-in-the-middle attacks or spoofing.

As an additional layer of defense in depth, Aurora PostgreSQL 17 enforces SSL/TLS by default (rds.force_ssl defaults to 1 for version 17 and later), so all connections from the application to the database are also encrypted in transit.

External communication

All external traffic flows through three layers, each enforcing HTTPS communication. Only port 443 is exposed externally.

CloudFront is the primary entry point for all external traffic. HTTP requests are automatically redirected to HTTPS (redirect-to-https viewer protocol policy). The minimum TLS version is TLSv1.3_2025, the strictest policy available.

AWS Global Accelerator provides an additional entry point for TCP and UDP traffic, both exclusively on port 443. No other ports are open. Health checks use the HTTPS protocol. The Global Accelerator is used only in cases where mTLS (mutual TLS) support is required, for example communicating with Arkea.

Application Load Balancer (ALB) sits inside the VPC and is not directly exposed to the internet. It listens exclusively on port 443 using the ELBSecurityPolicy-TLS13-1-3-2021-06 security policy, which enforces TLS 1.3. There is no HTTP listener configured. Communication from CloudFront to the ALB origin is set to https-only with a minimum of TLS 1.2.

In summary, the only port open to external traffic is 443 (HTTPS), enforced at every layer of the stack with a minimum of TLS 1.3 for client-facing connections.

Mastercard communication

Green-Got uses separate Mastercard communication paths for payment-network traffic and file exchange. Both use secure transport; Green-Got does not operate its own Mastercard File Express server.

Payment-network traffic is exchanged with Mastercard over AWS Direct Connect. The PCI DSS Scoping Evidence identifies AWS Direct Connect as the dedicated private network connection for Mastercard traffic. Mastercard mTLS material is maintained in the Key and Certificate Inventory: client private key, client certificate chain, and Mastercard server CA bundle are stored as secret parameters and used for mutual TLS with Mastercard network services.

Mastercard File Express is used as a Mastercard-hosted file exchange service. The Mastercard File Express server uses SFTP/SSH transport and SSH key-based authentication. File Express therefore provides secure SFTP transport to a Mastercard-hosted endpoint.

The Key and Certificate Inventory separately records partner SFTP SSH keys and host keys for Mastercard SFTP connections, including client authentication and host-key pinning for SFTP transfers.

Exceet communication

Green-Got communicates with Exceet through an externally hosted SFTP endpoint for card manufacturing and personalization exchanges. The Key and Certificate Inventory records partner SFTP SSH private keys, public keys, and server host public keys for Exceet. These keys provide client authentication and server host-key pinning for the SFTP transport.

Exceet payloads have an additional file-encryption layer. The Cipher Suite and Protocol Inventory records OpenPGP/PGP for partner payload encryption before transmission, and the Key and Certificate Inventory records Exceet OpenPGP public/private key material as secret parameters for file encryption and decryption.

This means Exceet transfers are protected by layered controls: SFTP/SSH transport to the externally hosted SFTP server, server host-key validation, SSH client-key authentication, PGP encryption of exchanged files.

Operational alert dashboard

Provide a screenshot of your current alert configurations from an application monitoring dashboard, such as Datadog or New Relic. This demonstrates how operational alerts are configured and monitored, and supports audit requirements related to incident detection and response.

Evidence

Currently setup alerts Alerts

Example of an individual alert configuration Alert config

How the team is informed about alerts triggering Devops Slack Channel

Operations Policy defines "critical" events

Create and add a table or other section in the Operations Security Policy that defines event type, severity, system, or other criteria that should be considered Critical and should activate the Incident Response Plan if observed. Note: Think about the most important Confidentiality, Integrity and Availability constraints/needs in your environment. Events that impact those business processes or systems are often (but not always) critical.

Evidence

Green-Got defines critical security events in the Operations Security Policy under Critical Security Events.

The policy classifies the following categories as critical for operational monitoring and incident response escalation:

Event typeCritical criteria
Confirmed unauthorized accessConfirmed unauthorized access to production systems, administrative accounts, customer data, regulated data, or security tooling.
Active exploitationEvidence of active exploitation, attacker persistence, command-and-control activity, malware execution, or unauthorized privileged activity.
Cardholder data exposureConfirmed or suspected unauthorized access, disclosure, movement, storage, or transmission of cardholder data or sensitive authentication data.
Critical security control failureFailure or loss of visibility for controls that protect the CDE, including logging, monitoring, intrusion detection, file integrity monitoring, encryption, access control, or network segmentation.
Severe service availability impactOutage, degradation, or data integrity issue affecting production service delivery, payment-card-related services, or incident response capabilities.
Insider threat or malicious internal activityEvidence that an employee, contractor, vendor, or partner performed or attempted malicious, unauthorized, or policy-violating activity affecting company systems or data.

The Incident Response Plan defines S1 critical severity for actively exploited risks. Critical security events activate the Incident Response Plan and are handled as S1 critical severity unless triage determines a lower severity is appropriate.

P1 security issues resolved

This test verifies that all security tasks labeled with priority level P1 in your task tracker are marked as completed within the required SLA (Service Level Agreement) timeframe.

Evidence

We are deactivating this Vanta test because it is not used to satisfy PCI DSS requirements.

Our current focus is PCI DSS scope. Linear does not support scoping this test to the relevant teams, so issues labeled as security by other teams also appear here even though they are outside the PCI DSS scope, which adds noise to the results.

P2 security issues resolved

This test verifies that security tasks labeled as Priority 2 (P2) in your integrated task tracker are marked as completed according to your defined SLA (Service Level Agreement).

Evidence

We are deactivating this Vanta test because it is not used to satisfy PCI DSS requirements.

Our current focus is PCI DSS scope. Also Linear does not support scoping this test to the relevant teams, so issues labeled as Security by other teams also appear here even though they are outside the PCI DSS scope, which adds noise to the results.

P3 security issues resolved

This test checks if all security-related tasks prioritized at P3 within your task tracker are marked as completed according to your configured SLA (Service-Level Agreement) deadlines for security tasks.

Evidence

We are deactivating this Vanta test because it is not used to satisfy PCI DSS requirements.

Our current focus is PCI DSS scope. Also Linear does not support scoping this test to the relevant teams, so issues labeled as security by other teams also appear here even though they are outside the PCI DSS scope, which adds noise to the results.

PAN

PAN Encryption or Obfuscation - Database

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-04-30
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material PAN protection change

All Primary Account Numbers (PANs) stored in PostgreSQL are persisted as envelope-encrypted binary values. The database does not store clear PAN.

PAN storage uses the data-at-rest cryptographic model described in 2_cryptography_key_management.md:

PAN matching and lookup use a keyed HMAC-SHA256 lookup value, which avoids decrypting PAN for equality checks. The application computes the keyed HMAC lookup value locally with PAN lookup HMAC keys loaded at startup from protected AWS Systems Manager Parameter Store SecureString values.

pan_db_encrypted

Change History

VersionDateAuthorChanges
1.02026-04-21julian@green-got.comDocument database PAN encryption and obfuscation controls.
1.12026-04-30julian@green-got.comAdd document control metadata and align PAN lookup HMAC wording with startup-loaded AWS Systems Manager Parameter Store secrets.

PAN is rendered unreadable anywhere it is stored

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-04-30
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material PAN protection change

Green-Got renders stored Primary Account Numbers (PANs) unreadable through application-side envelope encryption backed by AWS KMS. PAN values are not stored in clear text in PostgreSQL. Database fields that contain PAN use the Encrypted<T> storage pattern and persist only an encrypted envelope as a binary value.

The data-at-rest encryption model is documented in 2_cryptography_key_management.md and key_encrypting_keys.md. The model uses the following controls:

pan_db_encrypted

Lookup and Obfuscation

PAN lookup does not require decrypting stored PAN. Green-Got generates a keyed HMAC-SHA256 lookup value from the PAN for matching and indexing. The application computes the keyed HMAC lookup value locally with PAN lookup HMAC keys loaded at startup from protected AWS Systems Manager Parameter Store SecureString values. The application identifies a PAN record without storing or comparing clear PAN values.

Application logs and default display paths use obfuscated sensitive values. PAN is displayed in masked form by default, as documented in pan_masked/index.md. Clear PAN is exposed only for authorized customer-facing (via an iframe) or operational flows that require the full value, and the clear value exists only transiently in memory.

Pan Logging

For logging, PAN values have an explicit masked display representation. Regular display and debug output are obfuscated by default, so routine logging does not expose the clear PAN.

fn test_pan_masking() {
    let pan = Pan::try_new("1234567890123585").unwrap();
    tracing::info!("{}", pan.display_masked());
    tracing::info!("{}", pan.to_string());
    tracing::info!("{pan:?}");
}

pan_logs_masked

Change History

VersionDateAuthorChanges
1.02026-04-21julian@green-got.comDocument PAN unreadable-at-rest controls and masked logging evidence.
1.12026-04-30julian@green-got.comAdd document control metadata and align PAN lookup HMAC wording with startup-loaded AWS Systems Manager Parameter Store secrets.

PAN Encrypted in Transmission

Provide evidence that Primary Account Number (PAN) is encrypted using strong cryptography whenever it is transmitted over open, public networks, including the internet, wireless technologies, cellular, and satellite communications.

Evidence

Green-Got encrypts PAN using strong cryptography whenever PAN is transmitted over open or public networks.

Public HTTPS communication is evidenced by the External vulnerability scan. The ASV scan covers the public green-got.co endpoint, records a passing scan result, and shows the public host exposes only ports 80 and 443. HTTP traffic on port 80 is redirected to HTTPS on port 443. This supports the public HTTPS transmission evidence for Green-Got’s customer-facing endpoint.

The Strong Cryptography Used evidence documents the Green-Got-controlled public HTTPS configuration and partner communication mechanisms:

For non-public partner communication paths, Green-Got communicates card data only with PCI DSS certified partners. This requires both parties to maintain compliant secure transmission controls, including the required secure protocols, keys, certificates, and operational evidence. Green-Got keeps logs and provides both log samples and relevant code excerpts for these communication paths when requested by the assessor.

The 2026 H1 PCI DSS Scoping Exercise records the PAN transmission paths and their transport security.

PAN detection response process

Please ensure your PCI policy or Incident Response Policy includes a section on how you will activate the incident response plan if PAN is detected in unapproved locations in your environment.

Evidence

Green-Got activates the Incident Response Plan for information security or data privacy events, including detection of PAN in an unapproved location.

The plan defines the reporting channels, severity assignment, escalation path, response process, documentation requirements, containment and remediation activities, and legal or regulatory review. When unexpected PAN storage is identified, the event is reported through the defined channels, triaged as an information security or data privacy event, assigned severity, investigated, contained, remediated, and documented in the incident record.

The related request for records of response actions is tracked separately in Risk - Incident Response Records Unexpected PAN Detected.

PCI DSS Roles and Responsibilities ⏱️

Provide documentation that outlines roles and responsibilities for individuals or teams involved in managing PCI DSS compliance. This includes defined responsibilities for security operations, data protection, vulnerability management, access control, and other relevant domains. The documentation must demonstrate that roles are assigned, understood, and maintained as part of your organization’s governance framework. Google Docs Template / Docx Template

Evidence

Green-Got maintains a Information Security Roles and Responsibilities Policy. The policy applies to Green-Got infrastructure, network segments, systems, employees, and contractors who provide security and IT functions.

The policy defines roles and responsibilities for the Green-Got community, the Board of Directors, Executive Leadership, CTO, Head of Risk, Cybersecurity Engineer, Developers/System Owners, Managers, and VP of Global Customer Support. These responsibilities cover security governance, PCI DSS and information security compliance oversight, risk management, access control, secure development, vulnerability and security operations, data retention and deletion, third-party risk management, employee qualification, policy acknowledgement, and incident or anomaly reporting.

The policy also documents compliance accountability. The Head of Risk and CTO measure adherence through reports, internal and external audits, and feedback to the policy owner. Exceptions require advance approval by the Head of Risk or CTO, and non-compliance is addressed with management and Human Resources.

PCI program ownership assigned

Ensure your roles and responsibilities template formally identifies an owner of PCI-DSS for the organization.

Evidence

PCI DSS program ownership is formally assigned through the Information Security Roles and Responsibilities Policy (Policy Owner: Fabien Huet, Effective Date: March 9, 2026).

The policy assigns joint accountability for information security compliance — including PCI DSS — to two roles:

Both roles share approval authority for policy exceptions and compliance measurement. Individual control ownership for each PCI DSS requirement is tracked in Vanta, where each test has a designated owner responsible for managing and evidencing that control.

PCI responsibilities represented in contractual language

Ensure that your standard MSA or other contractual documentation includes language that you are responsible for maintaining PCI-DSS compliance as applicable and as part of your ongoing security commitments to your customer and provide a copy of the MSA or a screenshot of the included language.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Requirements 12.9, 12.9.1, and 12.9.2 are additional requirements for service providers only. They require TPSPs to provide written acknowledgments to their customers confirming responsibility for the security of account data, and to support customers’ requests for PCI DSS compliance status and responsibility information.

Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers. Its customers are not merchants or PCI-assessed entities that rely on Green-Got to meet their own PCI DSS obligations. These requirements address TPSP-to-assessed-entity relationships, not consumer banking relationships.

Green-Got does not maintain a standard MSA containing PCI DSS responsibility language under this control, as its customer base does not include PCI-assessed entities.

POI tampering checks and inventory

Provide evidence that POI Tampering, checks are completed at periodic intervals and follow a formalized process. These checks should occur based on an inventory of devices that includes: Make and model of the device Location of device Device serial number or other methods of unique identification

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

POI tampering identification training

Provide evidence of formal training that employees who perform tamper checks undergo to identify tampered POI devices.

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

POS OS access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login attempts on 3 (or all if fewer than 3) different POS/POI system types using the vendor-supplied default accounts and passwords. The evidence must specify the default accounts/passwords used.

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

Password complexity minimums ❌

Screenshot or other evidence that passwords for all in scope systems must meet minimum complexity requirements as defined in Access Control or all other applicable policies Note: This is often defined as “a minimum of 8 characters including letters, numbers, symbols or a combination of all) but should be defined based on your organizations risk tolerance and any compliance or regulatory requirements that must be met

Password reuse controlled

Evidence

This evidence covers AWS account-level password reuse controls and the Green-Got Back Office authentication flow.

AWS account password settings are managed through infrastructure as code. The AWS account password policy enforces the following controls for IAM users with console passwords:

This configuration prevents reuse of recently used passwords for any AWS IAM user account that is configured with a console password.

Human interactive access to AWS is performed through AWS Identity Center with individual user identities. Service and workload access is performed through IAM roles or other non-interactive credentials rather than shared console passwords.

The Green-Got Back Office does not use passwords for operator authentication. Back Office access is passwordless and relies on individually authenticated Tailscale access together with WebAuthn passkeys. Because no passwords are used in this authentication flow, password reuse controls are not applicable to the Back Office.

The Back Office authentication mechanism is documented in MFA enabled for in-scope systems.

Passwords used for access to CDE applications and systems are configured securely ❌

Provide screenshots or exports from your authentication systems (e.g., identity provider, local OS, cloud platforms, or VPN) demonstrating that password configurations are set to:

If different platforms use different settings, provide evidence for each relevant system. Clearly indicate if technical constraints apply and how they are documented or mitigated.

Payment Application access defaults changed

Vendor-supplied default accounts and passwords Provide screenshot from one user showing failed login attempts using the vendor-supplied default accounts and passwords. The evidence must specify the default accounts/passwords used.

Evidence

Our payment application is custom-developed in-house. It is not a vendor-supplied product and does not ship with default accounts or default passwords.

Authentication to the application is handled via our own identity system with credentials created per-user. There are no pre-configured accounts.

The source code repository is available at Github Repository, and the software inventory is documented in Custom Developed Software Inventory.

Since the application is custom-built, the concept of “vendor-supplied default credentials” does not apply.

Payment page script protection

Provide evidence showing that

Explaination

We are neither a merchant nor do we implement a payment gateway. There is no payment page where the entering of PAN, CVC or related pan data is required

Payment page tamper detection ⏱️

Provide evidence that mechanisms are in place to detect and respond to tampering with any in scope payment pages.

Explaination

We are neither a merchant nor do we implement a payment gateway. There is no payment page where the entering of PAN, CVC or related pan data is required

Penetration test remediation ❌

Provide evidence demonstrating that issues identified during your most recent periodic penetration test have been addressed.

Acceptable Evidence and Implementation Guidance

If No Issues Were Found

Provide documentation such as meeting minutes or internal communications confirming the review and acknowledgment of the penetration test results.

Penetration test report ⏱️

Provide documentation or a report showing security findings in your recent periodic penetration test.

Evidence

Green-Got has not yet completed the PCI DSS penetration test cycle for the current audit period. The penetration test is scheduled for the PCI DSS audit remediation phase. The planned final evidence package includes the test scope, tester qualification and independence evidence, execution records, findings, remediation records, and retest results.

Green-Got keeps CDE and SAD processing logically separated in the backend through the core_banking domain, which concentrates cardholder-data handling, sensitive authentication data handling, and the access paths for those data flows. This logical separation supports auditability and limits where CDE/SAD handling is implemented. It is not presented as a network segmentation control that excludes the rest of the backend environment from PCI DSS scope. As documented in the Cardholder network diagram process, the backend environment that stores, processes, transmits, or affects the security of cardholder data remains in scope for PCI DSS.

Planned Testing Approach

Green-Got’s planned penetration-test resourcing covers both internal and external penetration testing using qualified internal resources with organizational independence from the systems being tested. If an internally independent and qualified tester is not available for a required test segment, Green-Got uses a qualified external third party. The tester does not need to be a QSA or ASV.

The penetration-testing methodology includes industry-accepted approaches, including OWASP application testing guidance, OSSTMM-style network testing coverage, manual validation, controlled exploitation where authorized, and automated discovery as supporting evidence. This reflects PCI DSS 11.4.1 guidance that vulnerability scanning alone is not a penetration test, that automated tools may support the work, and that penetration testing remains a highly manual process performed by a qualified tester using simulated attack methods.

The planned scope includes:

Green-Got is evaluating Nuclei as one supporting tool for repeatable application and infrastructure checks. Nuclei is already partially integrated in Green-Got’s CI tooling and is documented as part of the vulnerability management evidence in Software vulnerabilities. Any Nuclei results used in the penetration test are reviewed manually and supplemented by tester-driven validation.

Testing Cadence and Tracking

Green-Got tracks the recurring internal and external penetration test cycle in Linear issue PCI-18: Perform and report annual pentest. The issue is configured as an annual recurring compliance task and covers the PCI DSS penetration test report and related remediation evidence package.

The current penetration-test cadence is annual, with additional testing after any significant infrastructure or application upgrade or change affecting the CDE, trusted networks, critical systems, or the security of cardholder data. This cadence covers internal penetration testing under PCI DSS 11.4.2 and external penetration testing under PCI DSS 11.4.3.

Green-Got does not use segmentation to isolate the CDE from out-of-scope networks or to reduce PCI DSS scope. The service-provider six-month cadence in PCI DSS 11.4.6 applies to segmentation-control penetration testing when segmentation is used to isolate the CDE from other networks. Because Green-Got does not currently use segmentation for scope reduction, that semi-annual segmentation cadence is not applicable to the current environment.

Final Report Contents

The planned final penetration test report identifies:

Green-Got retains penetration testing results and remediation activity results for at least 12 months. Evidence of remediation is maintained in the related Penetration test remediation evidence document.

Penetration testing used to validate logical environment separation

Evidence of a formal penetration test occurring at least every 6 months that validates the effectiveness of logical separation controls used to separate customer environments.

Guidance: This can (and likely should) be part of your ongoing scope validation processes. If you use segmentation to reduce scope it must be validated using a penetration test, so doing that same test once every 6 months and including the ability for Tenant escape or cross-tenant leakage is the easiest way to handle this.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.1.4 applies to multi-tenant service providers, requiring penetration testing at least every six months to validate logical separation controls between customer environments. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not host separate customer environments.

Green-Got does not perform cross-tenant separation testing under this control. Penetration testing for Green-Got’s own PCI DSS environment is documented under the standard penetration testing evidence set.

Penetration testing validates CDE segmentation ❌

Penetration test report demonstrating that cardholder data environment segmentation was specifically validated (annually for merchants, every 6 months for service providers). If segmentation is not used to isolate cardholder data environment, use the ‘Deactivate’ button and include an explanation.

Evidence

This Vanta document is not applicable to Green-Got’s current PCI DSS scope. The appropriate action in the Vanta UI is to use Deactivate with this explanation.

Green-Got keeps CDE and SAD processing logically separated in the backend through the core_banking domain, which concentrates cardholder-data handling, sensitive authentication data handling, and the access paths for those data flows. This logical separation supports auditability and limits where CDE/SAD handling is implemented. It is not presented as a network segmentation control that excludes the rest of the backend environment from PCI DSS scope. As documented in the Cardholder network diagram process, the backend environment that stores, processes, transmits, or affects the security of cardholder data remains in scope for PCI DSS.

Green-Got does not use segmentation to isolate the CDE from other networks or to reduce PCI DSS scope. As a result, PCI DSS 11.4.5 and 11.4.6, which apply only when segmentation is used to isolate the CDE from other networks, are not applicable in the current scope.

Green-Got’s applicable penetration testing evidence for PCI DSS 11.4.1, 11.4.2, and 11.4.3 is documented in Penetration test report. Remediation and retest evidence for PCI DSS 11.4.4 is documented in Penetration test remediation.

Personnel with Permission to Move PAN ⏱️

Provide evidence that only explicitly authorized personnel are permitted to copy and/or relocate Primary Account Number (PAN) using remote-access technologies. The evidence must show that such access is documented, justified by a defined business need, and restricted to those who require it.

Evidence

Remote-access technologies refer to things like SSH here, we don’t allow SSH into our production environment for that reason there is no further evidance to provide.

Physical Asset & Storage Security

Evidence of secure storage systems and processes in place to manage physical access to digital, hard copy, archival, and backup copies of Microsoft data. (i.e. secured office locations, encrypted hard drives, etc) and that chain of custody is maintained and tracked when transporting or destroying physical media such as flash drives, laptops, etc.

Evidence

Green-Got operates a cloud-native environment and does not store customer or cardholder data on physical media or endpoint devices. All sensitive data is stored within AWS-managed services.

Employee devices (laptops) are the only physical assets used and are secured through standard endpoint security controls, including full disk encryption, screen locking, and access controls. Staff operate under a remote-first model, with no corporate office or physical storage locations.

As no physical media containing customer data is created, stored, or transported, formal chain-of-custody procedures for such media are not applicable. Device lifecycle management (including provisioning and decommissioning) is handled through internal processes to ensure secure handling of company assets.

Physical media is handled securely

Evidence that cardholder data stored on physical media is protected throughout its lifecycle.

Guidance: If CHD is stored on physical media (paper, electronic media), provide the following evidence:

If no physical media is used to store cardholder data, use the ‘Deactivate’ button and include an explanation.

Evidence

Green-Got does not store cardholder data on any physical media (including paper, removable media, or endpoint devices such as employee laptops). All cardholder data is stored exclusively within AWS-managed services (e.g., Amazon Aurora and Amazon S3).

As a result, no physical media containing cardholder data is created, stored, transported, or destroyed by Green-Got. Physical storage media and infrastructure are managed by AWS in accordance with their compliance programs.

Therefore, controls related to handling and lifecycle management of physical media containing cardholder data are not applicable.

Physical security - Retail/Physical Location - termination records

Provide evidence (e.g. 3 recent termination notifications from HR and a screenshot of the list of users with badge access) clearly showing that physical access to the data center is removed after employees leave the organization.

Evidence

Green-Got operates as a cloud-native organization with no physical infrastructure in scope for PCI DSS. All cardholder data environment (CDE) components are hosted entirely within Amazon Web Services (AWS). Green-Got does not operate data centers, retail locations, or any other physical facilities that store, process, or transmit cardholder data. No Green-Got employees have badge access or any other form of physical access to in-scope locations.

Physical security of the CDE infrastructure, including personnel access provisioning and deprovisioning for data center facilities, is fully managed by AWS under their own PCI DSS compliance program. This responsibility is documented in the AWS PCI DSS Attestation of Compliance (AOC), available in Vanta.

As a result, there are no physical access termination records to maintain on Green-Got’s side — the requirement is satisfied through AWS’s compliant controls as referenced in the shared responsibility model.

Physical security - User access approvals

Please provide evidence of formal approvals for physical access for users or employees with access to physical locations that are in scope.

Evidence

Green-Got operates as a cloud-native organization with no physical infrastructure in scope for PCI DSS. All cardholder data environment (CDE) components are hosted entirely within Amazon Web Services (AWS). No Green-Got employees have physical access to any systems that store, process, or transmit cardholder data.

Physical security of the CDE infrastructure, including formal approval processes for physical access to data centers, is fully managed by AWS under their own PCI DSS compliance program. This responsibility is documented in the AWS PCI DSS Attestation of Compliance (AOC), available in Vanta.

As a result, there are no physical access approvals to maintain on Green-Got’s side — the requirement is satisfied through AWS’s compliant controls as referenced in the shared responsibility model.

Physical security - User access list

Please provide evidence of a list of users or employees with access to physical locations that are in scope.

Evidence

Green-Got operates as a cloud-native organization with no physical infrastructure in scope for PCI DSS. All cardholder data environment (CDE) components are hosted entirely within Amazon Web Services (AWS). No Green-Got employees have physical access to any systems that store, process, or transmit cardholder data.

Physical security of the CDE infrastructure is fully managed by AWS under their own PCI DSS compliance program. AWS maintains and enforces all physical access controls to data centers, including access lists, biometric authentication, and surveillance. This responsibility is documented in the AWS PCI DSS Attestation of Compliance (AOC), available in Vanta.

As a result, there is no physical access list to maintain on Green-Got’s side — the requirement is satisfied through AWS’s compliant controls as referenced in the shared responsibility model.

Physical security - User access permissions

Please provide evidence of the access permissions of users or employees with access to physical locations that are in scope. This should include physical access, and access to any systems used for adding or removing users from the access control system such as badge creation or biometric authentication tools.

Evidence

Green-Got operates as a cloud-native organization with no physical infrastructure in scope for PCI DSS. All cardholder data environment (CDE) components are hosted entirely within Amazon Web Services (AWS). No Green-Got employees have physical access to any systems that store, process, or transmit cardholder data, nor access to any physical access control systems such as badge creation or biometric authentication tools for in-scope locations.

Physical security of the CDE infrastructure, including all access permissions, access control systems, and their management, is fully handled by AWS under their own PCI DSS compliance program. This responsibility is documented in the AWS PCI DSS Attestation of Compliance (AOC), available in Vanta.

As a result, there are no physical access permissions to maintain on Green-Got’s side — the requirement is satisfied through AWS’s compliant controls as referenced in the shared responsibility model.

Physical security is outsourced to a compliant Service Provider

Evidence that physical security is covered under CSP’s compliant Attestation of Compliance. Guidance: In most cases where the CDE is hosted by a CSP physical security of systems will be covered under the CSP’s Attestation of Compliance. Review and upload CSP’s AOC and/or PCI responsibilities matrix demonstrating that physical security is their responsibility. AOCs/matrix can typically be downloaded from the CSP’s customer compliance portal or requested through the CSP’s Customer Support function.

Evidence

Physical security is provided by AWS. AWS Attestation of Compliance is available in Vanta.

Physical security monitoring

Provide evidence that individual physical access to sensitive areas within the CDE is monitored with either video cameras or physical access control mechanisms (or both) as follows:

Guidance: Think screenshots of video recording consoles, examples of access logs from door locks, pictures of locked doors or camera angles, etc.

Evidence

Green-Got does not maintain physical access to the cardholder data environment. The CDE is hosted in AWS cloud infrastructure, and physical access to the underlying facilities and hardware is managed by AWS.

Planned access review

Provide a ticket or calendar invite demonstrating that a periodic access review was planned and completed. Guidance: For additional information on how to fulfill this evidence request, see this guiding document: Google docs template / Docx template. Note: Current access data can be exported from the Access page

Evidence

A Linear issue was created in the PCI DSS team on March 31, 2026 to track the periodic access review. The ticket records the review cadence, the Vanta evidence request, and the completion checklist used to execute the access review.

Linear issue for planned access review

Point-of-Sale/Point-of-Interaction devices are secured and protected

Evidence that POS/POI devices are inventoried, inspected for tampering or substitution, and employees are trained in best practices for protecting devices. Guidance: For entities with physical point-of-sale devices (including kiosks, handheld devices, or connected POS/POI systems), provide screenshots or supporting evidence demonstrating:

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

Prevention of PAN Reconstruction ⏱️

If both hashed and truncated versions of the same Primary Account Number (PAN) exist in the environment, provide evidence that appropriate controls are in place to prevent these values from being correlated and used to reconstruct the original PAN. If only one representation (hashed or truncated) is stored, indicate this clearly and provide documentation supporting that only a single form is retained.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-04-30
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material PAN lookup control change

Green-Got does not store an unkeyed hash of PAN. PAN is stored in encrypted form and, where lookup is required, represented by a keyed HMAC-SHA256 lookup value. Masked PAN values are used for display and support workflows.

The retained PAN representations are:

The HMAC value is not a standalone hash. It is produced with a secret key and is not reversible without the original PAN and the HMAC key. The masked PAN does not contain enough digits to reconstruct the full PAN. Correlating the keyed HMAC lookup value with the masked display value does not reconstruct the encrypted PAN or reveal the missing digits.

Access to these representations is separated by role and purpose:

Change History

VersionDateAuthorChanges
1.02026-04-20julian@green-got.comDocument retained PAN representations and reconstruction-prevention controls.
1.12026-04-30julian@green-got.comAlign PAN lookup HMAC access controls with application-managed lookup secrets stored in AWS Systems Manager Parameter Store and loaded at startup.

Primary Account Number (PAN) is masked when displayed

Screenshot or other evidence demonstrating that PAN is masked (minimum first six/last four digits) when displayed. If no PAN is displayed, use the ‘Deactivate’ button and include an explanation.

Evidence

Green-Got displays card PAN in masked form by default.

Masked PAN display

Green-Got is a bank and has to provide cardholders with access to their own card details. The application displays the clear-text PAN only when the authenticated cardholder explicitly requests to view the card details. That reveal is part of the customer-facing banking functionality and is limited to the cardholder’s own card.

Cardholder-requested clear PAN display

Outside of the cardholder-requested reveal flow, displayed PAN is masked.

Privileged activity logged ⏱️

Provide evidence that all activity by users or services with privileged access are logged in a centralized system such as the native logging portal (e.g., Google Admin Activity Audit Logs) or SIEM tool (e.g., Splunk or Datadog). Guidance: This applies to all root, system, admin, or actions taken as part of a privilege escalation response such as Windows or macOS User Account Control (UAC). Evidence could include:

Screenshots of logs or logging configurations demonstrating privileged user activity is logged Exports of logs from the native logging portal or centralized SIEM tool. Log exports should be comprehensive and include, at a minimum:

Source Timestamp Activity Host or System Data.

Evidence

For AWS where your production environment lives we are relying on AWS native Cloudtrail

Cloudtrail list

Cloudtrail detail

For Github we rely on Github’s native audit logs

Github

Privileged user lists generated ❌

Provide a screenshot or system export showing the administrative users for all in-scope systems and applications. You should consider users of all types, including business partners, service accounts, vendor and third party accounts, contractors, and employees. Guidance: Typical in-scope systems include:

  1. Background checkers
  2. Cloud providers
  3. Communication platforms
  4. CRM platforms
  5. Database/Data warehouse providers
  6. Endpoint security tools
  7. HRIS
  8. Identity providers
  9. MDM tools
  10. Vulnerability scanners
  11. SIEM tools
  12. Version control systems
  13. DevOps tools
  14. Document repositories Guidance: Ensure that the privileged access configured for your in-scope systems is based on active and current roles & responsibilities of those assigned individuals. If there are newly added individuals, ensure that you followed your access provisioning process to create / modify the accounts, and removed any personnel that did not require the access anymore. Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% of your privileged user accounts, as randomly selected and requested by your auditor. Note: This evidence is only required for admin users of tools that have not been connected to Vanta via the integrations page. For tools that have been integrated into Vanta, no additional evidence is needed for this document. A template can be found here: Google docs template

Process is in place to review logs daily ⏱️

Evidence that logs for all in-scope systems are reviewed daily. Guidance: Upload evidence of daily log review process (e.g. screenshot of Slack channel or email distribution list where events are sent for review). Include a sample ticket or tickets demonstrating that a followup was performed for anomalous events.

Evidence

We use SigNoz for short-lived operational observability data, including application traces, metrics, and application logs retained for 90 days.

We use ClickHouse for long-term audit logging, and those audit logs are reviewed through ClickStack / HyperDX. Audit-focused alerts are configured there, while operational fixed-threshold and anomaly-based alerts are configured across traces, logs, and metrics. When an alert is triggered it is sent into a dedicated Slack channel, where a team member reviews the alert and investigates the cause of the issue.

Process is in place to review logs periodically ❌

Provide documentation demonstrating that security logs for all system components not included in PCI DSS Requirement 10.4.1 are reviewed periodically, as defined by the targeted risk analysis. Examples of system components that are not included in PCI DSS 10.4.1:

Processes are in place to detect and respond to critical security control failures in a timely manner ❌

Evidence that systems providing critical security functions are continuously monitored. Guidance:

  1. Provide screenshots of SIEM system demonstrating that relevant systems providing critical security controls are monitored continuously. Systems may include the following:
  1. If applicable, upload tickets demonstrating that control failures were detected and resulted in a followup.

Prohibited Data - Database 📬

1-) Provide a sample of every single audit log type generated by the database(s) (e.g. transaction, history, debugging, error logs) 2-) Provide the schema(s) of the database(s) (name of all tables and their fields) 3-) Specify all database name(s) and their corresponding tables storing full PAN 4-) For all tables storing PAN, provide a sanitized screenshot showing the content of the fields within each table. System types in scope: • Provide an inventory of your Databases

Prohibited Data - Environment 📬

1-) Provide a sanitized sample of incoming transaction data showing the data elements captured. 2-) Provide a sample of every single audit log type generated by the application (e.g. transaction, history, debugging, error logs) 3-) Provide a sample of every single file type generated by the application (e.g. History files, Trace files)

Prohibited Data - Other systems 📬

Provide a sample of audit logs generated by other components of your environment (e.g. netflow, web app payment page activity or other application logs, debugging, error logs) to demonstrate that you are not capturing or storing SAD anywhere else by design (or inadvertently).

Proof of completed access review ❌

Documents or a report from a recent periodic access review of all in-scope components, including data stores, cloud infrastructure, version control system, etc. Guidance: For additional information on how to fulfill this evidence request, see this guiding document: Google docs template / Docx template. Current access data can be exported from the Access page. If you have the Access Reviews product, you can perform the review by creating a schedule or adhoc Access Review on the Reviews page. Note: This evidence is not required if using Vanta for access reviews.

Proof of media/device disposal

Provide evidence that physical media is disposed of properly through certificates of sanitization or destruction. Guidance: NIST 800-88 provides guidelines for media sanitization, including methods and techniques for differing types of media. As an example, PCI DSS Requirement 9 requires cardholder data on electronic media be rendered unrecoverable when deleted such as through a secure wipe program or be physically destroyed. There are many reputable companies which provide physical media destruction services and will issue certificates of destruction upon completion, such as Data Destruction Corporation. In lieu of a third-party performing data destruction, you can perform it internally and leverage the following sample certificate: Sample Certificate of Destruction: Google docs template / Docx template. Note: Test may be marked as not relevant if no media disposal is required and has not occurred.

Evidence

Green-Got does not store cardholder data on any physical media or endpoint devices (such as employee laptops, removable media, or local system disks). All cardholder data is stored exclusively within AWS-managed services (e.g., Amazon Aurora and Amazon S3).

As a result, no physical media containing cardholder data exists within the organization that would require sanitization or destruction. Physical infrastructure and underlying storage media are managed by AWS in accordance with their compliance programs.

Because no physical media containing cardholder data is used or disposed of by Green-Got, this control is not applicable.

Provider and customer access across environments is restricted

Evidence that the organization cannot access its customers’ environments without authorization and that customers cannot access the provider’s environment without authorization. Guidance: This can be done in many ways including contractual language, email confirmation, support tickets, or PAM (Privileged access management) tooling.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.1.1 applies to multi-tenant service providers, requiring logical isolation between the provider environment and each customer’s environment. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not host separate customer PCI environments.

Green-Got’s PCI DSS environment serves Green-Got’s own banking service exclusively. No provider-versus-customer environment boundary of the type addressed by this control exists.

Publicly accessible physical infrastructure is protected

Provide evidence that publicly accessible network jacks, wireless access points or other infrastructure is disabled, difficult to access, or otherwise protected from tampering.

Evidence

Green-Got operates as a cloud-native organization with no physical infrastructure in scope for PCI DSS. All cardholder data environment (CDE) components are hosted entirely within Amazon Web Services (AWS). Green-Got does not own, operate, or maintain any network jacks, wireless access points, or other physical network infrastructure that stores, processes, or transmits cardholder data.

Physical security of the CDE infrastructure, including protection of network hardware and physical access points within data centers, is fully managed by AWS under their own PCI DSS compliance program. This responsibility is documented in the AWS PCI DSS Attestation of Compliance (AOC), available in Vanta.

As a result, there is no publicly accessible physical infrastructure to protect on Green-Got’s side — the requirement is satisfied through AWS’s compliant controls as referenced in the shared responsibility model.

R-1074 - Management - Security Policy Acknowledgement Records (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

For a sample of employees selected by the assessor provide evidence that verifies the personnel have acknowledged that they have read and understand the information security policy within the past 12 months

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1147 - Third Party - Contracts Written Agreements

Provide applicable written agreements for all vendors/service providers where cardholder is shared or that could affect security of cardholder data or the CDE that includes an acknowledgement from the third-party that they are responsible for the security of account data the they possess or otherwise store, process, or transmit on behalf of the entity, or to the extent that they could impact the security of the entity’s CDE.

Note: Supplier/vendor agreements Vanta Evidence instructs Client to provide 1 example service provider contract. Custom Request is needed to request the remainder.

Evidence

Green-Got maintains the following written agreements for third-party providers that receive account data or affect the security of the CDE:

For cloud providers, Green-Got uses standard customer terms rather than bespoke agreements. For AWS this agreement can be found here AWS Customer Agreement.

R-1388 - Incidents - Notification, Response, and Resolution (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Incident tickets that include resolution details for a sample of security incidents (S2)(A1).

Note: D11 - Incident report or root cause analysis in Vanta Evidence instructs user to select and provide 1 example. PCI QSA must select select sample sets for this testing procedure.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1408 - Change Management - Network Infrastructure Changes (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Change control records for the specified sample of changes made to network infrustructure that include changes to network connections showing proper approvals and managed according to PCI Req 6.5.1.

Note: Network & CSP - Change tickets sample Vanta requests 3 most recent change tickets. PCI SSC requires QSAs to perform a systematic sampling methodology.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1409 - Change Management - Network Rule Set Changes (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Change control records for a sample of rule set configuration changes made to any firewall, router, cloud or virtualization network controls, or any other network security control (NSC) showing changes were appropriately approved and authorized.

Note: Network & CSP - Change tickets sample Vanta requests 3 most recent change tickets. PCI SSC requires QSAs to perform a systematic sampling methodology.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1559 - Monitoring - Log Forwarding ⏱️

Provide screenshots or system outputs showing audit log files (including those for external-facing technologies) are being promptly backed up to a secure, central, internal log server(s) or other media that is difficult to modify. This evidence should show the frequency or time it takes to backup the log records.

Note: This request focuses on evidence of the log forwarding systems, while existing Vanta requests have a broader scope of log protection and retention.

Evidence

Green-Got forwards audit and infrastructure logs into ClickHouse, which acts as the central log storage and review system for PCI-relevant logs. ClickHouse Cloud performs managed backups of the log store.

The cloudtrail_events table stores AWS CloudTrail management events in ClickHouse and is part of the central audit log store.

ClickHouse CloudTrail events table

The ClickHouse Cloud backup configuration shows:

ClickHouse Cloud backups

R-1575 - Access Control - User Access Authorization Forms (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

For a sample of user accounts, including privileged user accounts, selected by the assessor, provide related user authorization forms and any related change forms (for user add, deletion, or modification of user IDs, authentication factors, or other identifier objects) showing documented approval by authorized personnel for the users assigned privileges

Note: Vanta evidence requests exists, but these requests user to supply two recent examples. PCI QSAs must select a Sample Set of users.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1576 - System Operations - Inactive After 90 Days Disabled (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide evidence that inactive user accounts over 90 days old are either removed or disabled from the sample of network or system components

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1579 - System Operations - Authentication Factor Protected with Cryptography (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide evidence that helps verify authentication factors, such as passwords, are unreadable (encrypted or hashed) during storage and transmission for a sample of system components

Note: Secure password storage and transmission & Vendor credentials used securely in Vanta requests this, but PCI QSAs are required to select system sample sets for this testing procedure.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1755 - Management - Incident Records (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide completed resolution documentation (e.g. incident ticket, incident report, etc.) for a sample of control failures/gaps, including evidence that the control failures/gaps were documented, root cause identified, and properly remediated/addressed to prevent reoccurrence

Note: Processes are in place to detect and respond to critical security control failures in a timely manner in Vanta requests the processes for detecting, but does PCI QSA is required to select a sample of security control failure events to obtain records for.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-1986 - Vulnerability Management - Internal Vulnerability Scan Reports ❌

Provide 4 quarters of internal vulnerability scan reports to verify that internal scans have occurred at least once every three months in most recent 12-month period. In addition, provide any subsequent rescans performed in the last 12-months to address high-risk or critical vulnerabilities identified on any initial quarterly scans.

Note: vulnerability-scan & Sample of remediated vulnerabilities & High and critical vulnerability rescan in Vanta Evidence only requests most recent vulnerability scan. A-LIGN needs to request 4 quarters worth of scans & rescans.

Evidence

R-2128 - System Operations - Virtualization Functionality Isolation ❌

Where virtualization technologies are used, provide system configuration evidence to verify that the systems with different functions requiring different security levels are managed in one of the following ways:

Evidence

R-2129 - System Operations - Insecure Services ❌

If any insecure services, protocols, or daemons, are utilized, provide configuration evidence that features are implemented to reduce the risk of using insecure services, daemons, and protocols (per required configuration standards).

Evidence

R-2131 - Data Management - Data Retention, Handling and Disposal (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide screenshot of file and system records on system components where account data is stored verifying that data storage amount and retention time does not exceed the requirements defined in the organizations data retention policy.

Note: Data is retained according business, legal, and regulatory requirements may suffice if no sampling is required. However, this task covers the standard sampling workflow expected for this control.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2133 - Data Management - Stored SAD Encrypted ❌

Provide system configuration and/or vendor documentation that verifies that all SAD stored electronically prior to completion of authorization is encrypted using strong cryptography while being temporarily stored.

Evidence

R-2134 - Data Management - Issuer Services Storing SAD Securely ❌

Additional requirement for issuers and companies that support issuing services and store sensitive authentication data: Provide system configurations that allow the assessor to verify that sensitive authentication data is stored securely.

Note: SAD is protected by issuers and those providing issuing services Vanta requests Issuer justification documentation, but does not specify the protection evidence required

Evidence

R-2140 - Data Management - Keyed Cryptographic Hashing ❌

Provide documentation about the key management procedures and processes associated with the keyed cryptographic hashes used in the environment that will allow the assessor to verify keys are managed in accordance with PCI 4.0 Req 3.6 and 3.7.

Note: All encryption processes are fully documented in Vanta requests generic control evidence, but specificity of this requirement will easily be overlooked by Client for the new PCI v4.0 2025 controls.

Evidence

R-2141 - System Operations - Disk-Level Partition-Level Encryption ❌

If disk-level or partition-level encryption (rather than file-, column-, or field-level database encryption) is used to render PAN unreadable, provide configuration evidence that allows assessor to verify that system if is configured according to vendor documentation and that the disk or partition encryption appropriately renders the PAN unreadable.

Note: Access to encrypted file systems containing CHD is not based on native OS authentication and keys are stored securely & PAN is rendered unreadable anywhere it is stored in Vanta requests generic control evidence, but specificity of this requirement will easily be overlooked by Client for the new PCI v4.0 2025 controls.

Evidence

R-2146 - Data Management - Cryptography In-Transit

For any transmission of PAN over open or public networks, provide system configuration evidence allowing the assessor to verify that strong cryptography and security protocols are implemented in accordance with the following:

Evidence

Green-Got uses strong cryptography and secure protocols for PAN transmissions over open or public networks.

The relevant control implementation for these flows is defined in the codebase as infrastructure and application code. The evidence package therefore consists of linked compliance documents plus relevant code excerpts from the repository for assessor inspection, rather than a separate manually maintained configuration artifact.

Public HTTPS communication is evidenced by the External vulnerability scan. The ASV scan covers the public green-got.co endpoint, records a passing scan result, and shows the public host exposes only ports 80 and 443. HTTP traffic on port 80 is redirected to HTTPS on port 443.

The Strong Cryptography Used evidence documents the Green-Got-controlled HTTPS configuration and partner communication mechanisms:

The Key and Certificate Inventory documents public TLS certificates, partner mTLS certificates, partner SFTP host keys, and partner file-encryption keys. Public AWS certificates use ACM DNS validation and managed renewal.

The Cipher Suite and Protocol Inventory records the cryptographic protocols and cipher suites currently used for these flows. The 2026 H1 PCI DSS Scoping Exercise records the PAN transmission paths and their transport security.

For non-public partner communication paths, Green-Got communicates card data only with PCI DSS-relevant partners and payment ecosystem providers. These integrations require both parties to maintain compliant secure transmission controls, including the required secure protocols, keys, certificates, and operational evidence. Green-Got maintains logs and provides log samples for these communication paths when requested by the assessor.

R-2152 - Data Management - Cryptography End-User Messaging PAN ❌

Provide vendor documentation that allows the assessor to verify that PAN is secured with strong cryptography whenever it is sent via end user messaging technologies.

Evidence

R-2154 - System Operations - Documented List of Systems Not At Risk of Malware

Provide a documented list of system components identified as not at risk of malware (if applicable)

Note: A process is in place to determine whether certain OS types do or do not require malware protection in Vanta request does not specify request user to supply a list.

Evidence

Green-Got maintains the current platform assessment in A process is in place to determine whether certain OS types do or do not require malware protection.

The following system component types are covered through the documented platform-specific malware controls:

System component typePlatform-specific scope decisionDocumented controls and monitoring
Linux workstationsLinux work machines are excluded from the workstation malware scanner scope through the platform assessment process.Endpoint security controls, patching, employee-workstation risk review, Fleet inventory, and endpoint status monitoring
Amazon Linux EC2 hosts used for ECSEC2 hosts are ephemeral and rotated; server-side malware risk is managed through host lifecycle controls and AWS-native detection.Amazon Inspector vulnerability scanning, GuardDuty Runtime Monitoring, GuardDuty Malware Protection for EC2 EBS volume malware scanning for EC2 and ECS-on-EC2 workloads, EC2 host rotation, vulnerability management workflow
Application containersContainers run with read-only filesystems at runtime; application container filesystems are not modified after deployment.Base image and dependency vulnerability scanning before deployment, read-only runtime filesystems, GuardDuty Runtime Monitoring on EC2 hosts, redeployment from controlled images

Green-Got distinguishes vulnerability scanning, runtime threat detection, and malware scanning for server-side systems. Amazon Inspector is the vulnerability scanning control for EC2 hosts and container images. GuardDuty Runtime Monitoring is the runtime threat detection control for EC2-hosted ECS workloads. The AWS-native malware scanning control for this architecture is GuardDuty Malware Protection for EC2.

Windows workstations use Microsoft Defender, and macOS workstations use Apple XProtect as documented in Malware configuration and Malware protections deployed. Linux workstations are the employee workstation operating system type excluded from the workstation malware scanner scope.

R-2157 - System Operations - Malware Detection Signature Update Settings

Provide screenshots of system anti-malware solution that allows the assessor to verify current state of anti-malware definitions and that they are current and are promptly deployed.

Note: Malware protections deployed in Vanta request is missing requests for Signature Definition Updates.

Evidence

Green-Got maintains anti-malware engine and definition updates through the platform controls documented in the submitted Malware configuration evidence.

PlatformSignature and engine update controlEvidence
Windows workstationsMicrosoft Defender receives engine and security intelligence updates automatically through Windows Update. Primo MDM enforces Defender configuration, including cloud-delivered protection.Primo MDM Defender configuration screenshots are attached below.
macOS workstationsApple XProtect and XProtect Remediator receive background updates from Apple independently of macOS system updates. XProtect is built into macOS and remains active on managed Macs.The submitted malware configuration evidence documents XProtect coverage and Fleet monitoring of related endpoint controls.
Linux workstationsLinux workstations are excluded from the workstation malware scanner scope through the platform assessment documented in A process is in place to determine whether certain OS types do or do not require malware protection.The platform assessment documents the scope decision and current Linux workstation controls.
EC2 hosts used for ECS workloadsGuardDuty Malware Protection for EC2 provides the AWS-native malware scanning control for EC2 and ECS-on-EC2 workloads. The service is operated by AWS and scans EBS volumes attached to EC2 instances.The submitted malware configuration and malware log retention evidence documents GuardDuty Malware Protection for EC2 coverage.

The current endpoint evidence bundle is attached in the malware configuration evidence:

The Defender MDM profiles used for Windows anti-malware configuration are attached here for reviewer convenience:

Primo - Microsoft Defender Security Configuration Primo - Microsoft Defender Maximum Protection

Green-Got treats Microsoft Defender, Apple XProtect, and GuardDuty Malware Protection for EC2 as managed anti-malware controls where signature, engine, and detection logic updates are delivered by the vendor service. These controls keep malware detection definitions current without manual update handling by end users.

R-2161 - System Operations - Malware Detection Scan Frequency Per TRA

Provide documented results of periodic malware scans that shows that the scans are currently performed at the frequency defined by the targeted risk analysis recommendations.

Evidence

Green-Got does not rely on a standalone periodic malware scan schedule as the primary control for PCI DSS Requirement 5.3.2. The current anti-malware implementation uses continuous or event-driven protection on workstation platforms identified as requiring anti-malware, with periodic scanning handled by the managed endpoint control itself.

The submitted Malware configuration evidence documents the current scan behavior:

PlatformCurrent scan behaviorSupporting evidence
Windows workstationsMicrosoft Defender performs real-time on-access scanning when files are opened, closed, renamed, or downloaded. Scheduled full-system scans run through the Defender configuration enforced by Primo MDM.Malware configuration, Primo Defender configuration screenshots, and the endpoint security evidence bundle.
macOS workstationsXProtect performs signature-based checks when applications are first launched, when applications change, and when signatures are updated. XProtect Remediator performs periodic background scans.Malware configuration and Fleet monitoring of related endpoint controls.
Linux workstationsLinux workstations are excluded from the workstation malware scanner scope through the platform assessment documented in A process is in place to determine whether certain OS types do or do not require malware protection.A process is in place to determine whether certain OS types do or do not require malware protection.
EC2 hosts used for ECS workloadsGuardDuty Malware Protection for EC2 provides AWS-native malware scanning for EC2 and ECS-on-EC2 workloads by scanning EBS volumes attached to EC2 instances.Malware configuration and Anti malware logs collected stored.

Because Green-Got’s workstation anti-malware controls use real-time or event-driven detection rather than only periodic scans, no separate targeted risk analysis is used to define a standalone periodic malware scan frequency for workstation coverage. Where periodic scans occur, they are part of the managed anti-malware control: Microsoft Defender scheduled scans are enforced through Primo MDM policy, and XProtect Remediator background scans are handled by macOS.

The current endpoint evidence bundle is attached in the submitted malware configuration evidence:

The Windows Defender MDM configuration evidence is attached here for reviewer convenience:

Primo - Microsoft Defender Security Configuration Primo - Microsoft Defender Maximum Protection

R-2167 - Change Management - Code Changes Software Vulnerability Review (SAMPLE) 📬

For any to bespoke and/or custom software used on any system component included in or connected to the CDE, provide evidence related to code changes made that allows the assessor to verify that code changes were reviewed prior to release into production or to customers and meets the following:

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2168 - Change Management - Code Changes Manual Code Reviews (SAMPLE) 📬

If manual code reviews are performed for any bespoke and/or custom software used on any system component included in or connected to the CDE, provide evidence related to code changes allowing the assessor to verify that code reviews meet the following:

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2179 - Change Management - System Changes (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

For a sample of changes selected by the assessor provide related change control documentation that allows the assessor to verify the documentation includes the following:

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2182 - Access Control - User Access System Settings (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

For a sample of user accounts and privileged user accounts, provide user access settings to critical applications, servers, workstations, and network devices within the environment, that allows the assessor to verify that access is based on:

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2184 - Access Control - System and Application Account Access Provisions ❌

Configuration settings showing all system and application accounts are assigned and managed per 7.2.5

Evidence

R-2186 - Access Control - System and Application Account Reviews ❌

Provide documented results from periodic reviews of application and system accounts and related access privileges.

Note: Proof of completed access review & Planned access review in Vanta focus on user access reviews, but don’t mention non-human account reviews.

Evidence

R-2187 - Access Control - Database Access Query Restrictions ❌

For querying repositories of stored cardholder data, including applicable databases, provide evidence showing the following:

Applicability: This is only applicable if users are provided programmatic access beyond DBA access. Otherwise, System Configuration - Database Sample likely can fulfill this request.

Evidence

R-2192 - Access Management - Terminated Users (SAMPLE) 📬

Provide information sources for terminated users (e.g. termination forms, user remove request ticket, or other related information sources that help to verify that terminated user IDs were required to be deactivated or removed from access lists upon termination.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2197 - Access Control - Password Rotation for Customer Accounts ❌

Additional testing procedure for service provider assessments only: For customer accounts (used to access payment card information) where MFA is not required for the users, provide evidence of one of the following:

Note: D45 - Non-consumer password settings are configured and managed securely in Vanta observed using 8.3.10 language, which will be replaced by 8.3.10.1 on 31 March 2025. Overall, consider Vanta Evidence Partially Related has requests for “non-consumer customer” v3.2.1 language and does not call out 90 day rotation explicitly.

Evidence

R-2203 - Access Control - Interactive Logon System and Application Account Password Policy ❌

If accounts used by systems or applications can be used for interactive login, provide password configuration showing change frequency and complexity of passwords/passphrases for application and system accounts

Evidence

R-2228 - Monitoring - FIM Monitoring Logs (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide configuration evidence of file integrity monitoring (FIM) or change-detection mechanisms that shows all monitored files and allows the assessor to verify that log files and/or log systems are being actively monitored to ensure that existing log data cannot be changed or truncated without generating alerts.

Note: D30 - Configure File Integrity Monitoring solution to detect changes to critical systems and files in the CDE Vanta request does not apply a “Sampling” method. Custom Request will be needed to apply PCI QSA expected sampling methodology.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2230 - Monitoring - Automated Audit Log Review

Provide screenshots and/or outputs showing the tools, methods, and/or software used to perform required automated audit log reviews.

Note: Process is in place to review logs daily & Operational alert dashboard in Vanta requests evidence of daily log reviews and alerting dashboards, but this should be validated through interview observations. Vanta Evidence does not specify “automated” requirements per this IRL request.

Evidence

Green-Got uses ClickStack / HyperDX for automated audit-log review. Audit and infrastructure logs are collected into ClickHouse and exposed in HyperDX, where alert rules review log streams for security-relevant events and anomalies.

Configured automated reviews include alerts for privilege escalation, authentication failures, infrastructure changes, and database incidents. Alerts are routed to the operational review channel for investigation.

HyperDX audit and operational alert overview

HyperDX audit and operational alert overview continued

The Logging and alerting is configured on PCI-impacting applications and systems evidence documents the log collection, alert configuration, and alert routing process in more detail.

The Operational alert dashboard evidence documents alert dashboard configuration and Slack-based alert delivery.

R-2236 - Monitoring - System Time Sync Settings (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide configuration settings from the systems providing time synchronization services showing that access to time data is restricted to only personnel with a business need.

Note: Time Synchronization Management - External time sources in Vanta instructs user to select their own sample of 3 central time servers. PCI QSA will need to select samples independently and per appropriate sampling methodology determined by QSA.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2240 - Vulnerability Management - Internal Vulnerability Scan Up-to-Date Configuration ❌

Provide configuration settings from the tool(s) used to perform internal vulnerability scanning, showing that the tool is kept up-to-date with the latest vulnerability information.

Evidence

R-2251 - System Operations - Intrustion Detection-Prevention Signatures Up-to-date ❌

Provide configuration evidence confirming intrusion-detection and/or intrusion-prevention system(s) that are actively in-use are configured to continue to keep all engines, baselines, and signatures up-to-date.

Evidence

R-2253 - System Operations - Covert CnC Malware Detection Vendor Documentation

Additional requirement for service providers only: Provide IDS/IPS vendor documentations that shows IDS/IPS techniques supported to detect, alert on/prevent, and address covert malware communication channels.

Note: D22 - IDS/IPS capabilities documentation in Vanta Evidence request does not call out covert CnC malware detection requirement. This may easily be overlooked by the user.

Evidence

Green-Got uses AWS GuardDuty as the IDS/IPS-aligned detection and alerting control for AWS workloads in the PCI environment. The submitted IDS/IPS capabilities documentation and Intrusion detection system installation evidence documents the GuardDuty deployment and its foundational data sources.

AWS vendor documentation describes GuardDuty finding types that detect covert malware communication channels and related behavior:

GuardDuty capabilityVendor documentationCnC relevance
EC2 C&C network activity detectionGuardDuty EC2 finding typesIncludes findings for EC2 instances querying IP addresses or domain names associated with known command-and-control infrastructure.
Runtime C&C activity detectionGuardDuty Runtime Monitoring finding typesIncludes runtime findings for EC2 instances and containers querying domains associated with known command-and-control servers.
DNS-based data exfiltration detectionGuardDuty EC2 finding typesIncludes DNS data exfiltration finding types, which cover malware or attacker behavior that uses DNS queries as an outbound channel.
Malware scan findingsMalware Protection for EC2 finding typesDocuments GuardDuty Malware Protection for EC2 findings generated when suspicious or malicious files are detected on EC2-backed workloads.
GuardDuty-initiated malware scansFindings that invoke GuardDuty-initiated malware scanDocuments that GuardDuty behavior findings related to EC2 instances or EC2-hosted container workloads invoke malware scanning when Malware Protection for EC2 is enabled.

GuardDuty findings are routed through Amazon EventBridge to an SNS topic that delivers email alerts to the security team. This provides the alerting path for CnC-related GuardDuty findings. Response and containment are handled through the incident response process, including investigation of the affected workload and replacement or isolation of affected EC2 resources when required.

R-2254 - System Operations - Covert CnC Malware Detection Configuration

Additional requirement for service providers only: Provide configurations from sampled systems showing methods to detect and alert on/prevent covert malware communication channels are in place and operating.

Note: D5 - Intrusion detection system installation in Vanta Evidence request does not call out covert CnC malware detection requirement. This will easily be overlooked by the Client.

Evidence

Green-Got uses AWS GuardDuty as the IDS/IPS-aligned detection and alerting control for AWS workloads in the PCI environment. GuardDuty is enabled for the AWS environment and configured to monitor the data sources used to detect suspicious network, DNS, API, runtime, and malware activity.

The submitted Intrusion detection system installation evidence documents the current GuardDuty configuration:

Configuration areaCurrent state
GuardDuty serviceEnabled across the relevant AWS regions for the PCI environment.
Foundational data sourcesGuardDuty consumes VPC Flow Log data, DNS query logs, and AWS CloudTrail events.
Runtime visibilityGuardDuty Runtime Monitoring is enabled for EC2 instances hosting ECS workloads, with the GuardDuty agent managed through AWS Systems Manager.
Malware scanningGuardDuty Malware Protection for EC2 is enabled for EC2 and ECS-on-EC2 workloads.
Alert routingGuardDuty findings route through Amazon EventBridge to an SNS topic that delivers email alerts to the security team.

These settings cover covert malware communication channels by detecting command-and-control activity, suspicious DNS activity, DNS-based data exfiltration, suspicious runtime behavior, and malware indicators on EC2-backed workloads. The related vendor documentation is provided in R-2253 - System Operations - Covert CnC Malware Detection Vendor Documentation.

An example GuardDuty findings view is attached in the submitted anti-malware log evidence:

GuardDuty findings

R-2261 - Uncategorized - Signed Acknowledgement of InfoSec Responsibilities

Provide documented evidence (e.g. such as a signed acknowledgement forms or output from an LMS), that allows the assessor to verify appropriate personnel must provide a personnel acknowledge of their information security responsibilities.

Evidence

Green-Got manages all policies in Vanta. For this evidence request, Green-Got uses the Information Security Policy acknowledgement record. Personnel assigned to this policy acknowledge it in Vanta as the record of their information security responsibilities.

Vanta maintains the acknowledgement records used for assessment, including the policy version, assignee, acknowledgement status, and acknowledgement timestamp.

R-2266 - Management - Annual Scoping Validation for All Entities

Requirement for Merchants (see R-2269 for Service Providers).

Provide documented results of scope reviews performed by the entity to verify that PCI DSS scoping confirmation activity includes all of the following (12.5.2.b):

Note: Vanta Evidence does not have a request that covers this explicitly. Currently points to Data Flow and Network Diagram processes. Excludes other bullet points.

Evidence

This requirement applies to merchants only. Green-Got is a service provider, not a merchant. The equivalent service provider requirement is covered under R-2269 (Management - Twice Annual Scope Validation for Service Providers), which requires scoping validation every six months per PCI DSS 12.5.2.1.

R-2269 - Management - Twice Annual Scope Validation for Service Providers

Additional requirement for service providers only: Provide documented evidence of scope reviews are performed by the entity every 6 months and show evidence that PCI scope review activity captures all of the following:

Note: Vanta Evidence does not have a request that covers this explicitly. Currently points to Data Flow and Network Diagram processes. Excludes other bullet points.

Evidence

Recurring Schedule

Green-Got maintains a recurring PCI DSS scope-validation task in Linear under the PCI DSS project. The task recurs every 6 months, with a new instance created automatically after each due date.

Linear recurring scope validation task configured to repeat every 6 months

Completed Reviews

PeriodDateConducted ByDocument
2026-H12026-04-05Enrico — Senior Software EngineerScoping Exercise 2026-H1

Scoping Exercise 2026-H1

PCI DSS Scope Review — 2026-H1

1. Review Metadata

FieldValue
Date of Review2026-04-05
Review Period2026-H1 (First semi-annual review)
Conducted ByEnrico — Senior Software Engineer
Applicable Requirements12.5.2, 12.5.2.1 (Service Provider)
Entity ClassificationService Provider — Level 1
Review TriggerInitial formal scope review per PCI DSS v4.0.1 Requirement 12.5.2. As a service provider, Green-Got is required to perform scope reviews at least once every six months and upon significant changes to the in-scope environment (12.5.2.1).
Next Scheduled Review2026-10-05 (6 months)

2. Methodology

This scoping review was conducted by walking through each of the minimum required activities defined in PCI DSS Requirement 12.5.2. The following activities were performed:

  1. Review of existing data flow documentation — The current cardholder data flow documentation (3_cardholder_data_flow.md, 1_card_holder_environment.md, 8_emv_transactions.md) was reviewed against the live environment to confirm all payment stages and acceptance channels are documented.
  2. Codebase analysis — The core banking application source code was analyzed to confirm all locations where account data is stored, processed, and transmitted. This included reviewing encryption patterns, database schemas, and external API integrations.
  3. Infrastructure review — AWS infrastructure configuration (VPC, security groups, route tables, load balancers, Direct Connect) was reviewed to confirm network boundaries, segmentation controls, and connectivity paths.
  4. Third-party service provider review — All external entities with access to the CDE or that store, process, or transmit cardholder data on behalf of Green-Got were identified and documented.
  5. Segmentation control verification — All segmentation mechanisms separating the CDE from out-of-scope environments were reviewed for correctness and effectiveness.
  6. Scope confirmation — All findings from the above activities were consolidated and confirmed to be reflected in the defined PCI DSS scope.

No significant changes to the in-scope environment were identified since the CDE was established.

3. Results

3.1 Data Flows — Payment Stages

Green-Got participates in the following payment stages:

Payment StageApplicableDescription
AuthorizationYesEMV chip transactions (ARQC/ARPC verification), online e-commerce (3D Secure via Apata), ATM PIN verification (PVV-based). All authorization requests are received from Mastercard via AWS Direct Connect and processed by the Core Banking Service.
CaptureYesTransaction capture data received from Mastercard network.
SettlementYesSettlement processing via Arkéa as principal member. Includes SEPA and instant payment flows.
Chargebacks/DisputesYesChargeback and dispute processing via Mastercard and Arkéa.
RefundsYesRefund processing via Mastercard and Arkéa.

3.2 Data Flows — Acceptance Channels

Acceptance ChannelTypeDescription
EMV Chip Contact with PINCard-PresentStandard inserted-card transaction with online PIN verification.
Contactless without PINCard-PresentTap-to-pay transactions below €50 threshold. Mastercard Europe limits: 5 consecutive transactions or €150 cumulative without PIN.
Contactless with PINCard-PresentTap-to-pay transactions at or above €50 threshold, requiring PIN entry.
E-commerce (3D Secure)Card-Not-PresentOnline transactions authenticated via 3D Secure through Apata (AAV validation).
Mobile Wallet (MDES)Card-Not-PresentMastercard Digital Enablement Service tokenized transactions via mobile wallets.
ATMCard-PresentCash withdrawal with online PIN verification via PVV.

Green-Got does not handle MOTO (mail-order/telephone-order) transactions or paper-based cardholder data flows.

3.3 Data Flow Diagram Status

The cardholder data flow diagram is documented in 3_cardholder_data_flow.md with a corresponding Excalidraw visual diagram. The document is currently at version 0.1, last updated 2026-04-02, owned by the Core Banking Team. The diagram covers all acceptance channels and payment stages listed above. It is currently in progress and pending formal CISO/QSA approval.

Action required: Finalize and formally approve the data flow diagram before assessment.

3.4 Account Data — Storage Locations

LocationData StoredEncryptionRetentionPurpose
Aurora Global Database (Postgres) — Primary: eu-central-1, Replica: eu-west-3PAN (encrypted), cardholder name, expiration date. SAD stored temporarily as part of issuer functions only (pre-authorization).AWS KMS envelope encryption: AES-256 KEK in KMS, KMS-generated data keys, application-side ChaCha20-Poly1305 encryption. Encrypted at rest and in transit (TLS).Per data retention policy; SAD deleted upon authorization completion.Primary persistent storage for all cardholder data.

No file-based backups contain clear cardholder data. Aurora Global Database handles replication natively between eu-central-1 and eu-west-3. No CHD exists in file systems, logs, or backup files outside the database.

3.5 Account Data — Processing Locations

LocationData ProcessedDescription
Core Banking Service (EC2)PAN, cardholder name, expiration date, SAD (PIN blocks, CVC, cryptograms)Monolith application. CHD is decrypted locally after AWS KMS unwraps the encrypted data key, held in memory for processing, re-encrypted for partner transmission, then zeroized from memory.
AWS KMSData encryption keys, KEKKey wrapping/unwrapping for internal storage encryption. Does not see clear CHD.
AWS Payment CryptographyPIN blocks, CVC, cryptograms, ARQC/ARPCTDES/AES cryptographic operations for partner communication. Processes SAD for card network operations.

3.6 Account Data — Transmission Paths

FromToData TransmittedTransport Security
MastercardCore Banking ServiceAuthorization requests, capture, settlement, chargebacks, refunds (PAN, SAD)AWS Direct Connect; encrypted in transit
Core Banking ServiceMastercardAuthorization responses, settlement dataAWS Direct Connect; encrypted in transit
Core Banking ServiceArkéaSettlement data, PIN blocks, key exchangesEncrypted in transit (mTLS via Global Accelerator)
ArkéaCore Banking ServiceKey material (ZMK, KPVV, PEK, KCVV, IMK), settlement confirmationsEncrypted in transit (mTLS via Global Accelerator)
Core Banking ServiceExceetCard personalization payloads (PAN, keys)Encrypted in transit
Core Banking ServiceApata3DS authentication data (AAV values)Encrypted in transit
Core Banking ServiceAWS KMSKey wrap/unwrap requestsAWS internal network (VPC endpoint)
Core Banking ServiceAWS Payment CryptographyCryptographic operation requests (PIN, CVC, cryptograms)AWS internal network (VPC endpoint)
Aurora Primary (eu-central-1)Aurora Replica (eu-west-3)Database replication (all data, encrypted)AWS native replication; encrypted in transit
Aurora (via NLB)ClickHouse CloudEncrypted data for data warehouseAWS Private Link; data is encrypted, no clear CHD

3.7 CDE and In-Scope System Components

3.7.1 Cardholder Data Environment (CDE)

Systems that directly store, process, or transmit clear cardholder data:

ComponentTypeRegionDescription
Core Banking ServiceEC2 instanceseu-central-1 (primary), eu-west-3 (failover)Monolith application processing all payment transactions. Stores, processes, and transmits CHD.
Aurora Global DatabaseManaged PostgreSQLeu-central-1 (primary), eu-west-3 (replica)Primary persistent storage for all cardholder data.
AWS KMSManaged HSMeu-central-1AES-256 KEK for data key wrapping. Used for internal storage encryption.
AWS Payment CryptographyManaged HSMeu-central-1TDES/AES operations for partner communication (PIN blocks, CVC, cryptograms).
VPC InfrastructureNetworkingeu-central-1, eu-west-3Subnets (3 per region, one per AZ), route tables, security groups.

3.7.2 Connected-to / Security-Impacting Systems (In Scope)

Systems that do not directly handle clear CHD but connect to or impact the security of the CDE:

ComponentTypeDescriptionJustification for In-Scope
AWS CloudFrontCDN / Entry pointPublic-facing entry point for HTTPS traffic.Routes traffic to the CDE via ALB.
AWS Global AcceleratorNetwork entry pointTCP/UDP entry point on port 443. Used for mTLS connections (e.g., Arkéa).Routes traffic to the CDE via ALB.
AWS WAF / ShieldSecurity controlDDoS protection and web application firewall.Provides perimeter security for the CDE.
AWS Application Load BalancerLoad balancerDistributes traffic to EC2 instances. Only accepts connections from CloudFront, Mastercard (Direct Connect), and Global Accelerator.Direct network path to CDE compute.
AWS Network Load BalancerLoad balancerProvides ClickHouse Cloud access to database replication.Connects to CDE database (encrypted data only).
AWS Direct ConnectNetwork serviceProvides Mastercard connectivity to the ALB.Carries CHD/SAD between Mastercard and the CDE.
Tailscale Mesh VPNRemote accessAdmin/developer access to VPC and application servers.Provides authenticated access into the CDE.
CI/CD PipelineDeploymentDeploys application code to CDE EC2 instances.Impacts configuration and security of CDE systems.
AWS CloudTrailAudit loggingLogs all API calls across the AWS environment.Provides security monitoring for the CDE.
AWS IAMIdentity managementControls access to all AWS resources including CDE components.Defines who and what can access the CDE.

3.7.3 Out-of-Scope Systems

Systems confirmed to be outside PCI DSS scope with justification:

ComponentJustification
Public APIsReceive only tokens and masked data. No clear CHD reaches these systems. Segmented from CDE via security groups.
Reporting PipelinesProcess only tokenized and aggregated data. No clear CHD.
Customer Support ToolsAccess only masked PAN (first 6 / last 4) and encrypted metadata. No clear CHD.
Third-Party AnalyticsReceive only anonymized and tokenized datasets. No clear CHD.
ClickHouse Cloud (Data Warehouse)Receives only encrypted data via NLB/Private Link. No clear CHD is transmitted to or stored in ClickHouse.
Staging EnvironmentSeparate AWS account. Does not store, process, or transmit production cardholder data.

3.8 Segmentation Controls

ControlTypePurposeVerification
AWS Security GroupsLogical (network ACLs)Control all network-level access between components. Only CloudFront, Mastercard (via Direct Connect), and Global Accelerator reach the ALB. Only the ALB reaches application servers. Only application servers and the NLB reach the database. The database has no outgoing traffic.Security group rules reviewed as part of this scoping exercise. Rules enforce least-privilege network access.
Tailscale GrantsLogical (VPN ACLs)Control internal access to the CDE. CI/CD, admins, and developers connect to application servers and use subnet routing for VPC access. All other users get HTTPS on port 443 only.Tailscale grant policies reviewed. Access is role-based.
AWS WAF / ShieldLogical (perimeter security)DDoS protection and web application firewall at the CloudFront and Global Accelerator entry points.WAF rules and Shield configuration reviewed.
Separate AWS AccountsLogical (account isolation)Production and staging environments run in separate AWS accounts with no cross-account access to CDE resources.AWS account structure and IAM policies reviewed.
AWS Direct ConnectPhysical (dedicated connectivity)Provides dedicated, private network connection for Mastercard traffic. Traffic does not traverse the public internet.Direct Connect configuration reviewed.

Green-Got does not operate a flat network. All communication paths are explicitly defined and restricted via security groups. There are no wireless networks in the CDE (fully cloud-hosted infrastructure).

3.9 Third-Party Service Provider Connections

TPSPAccount Data SharedPurposePCI DSS Assessed?Direct CDE Access?Connection Type
MastercardPAN, SAD (cryptograms, CVV, transaction data)Payment network routing for authorization, capture, settlement, chargebacks, refundsYesYesAWS Direct Connect to ALB
ArkéaPAN, PIN blocks, settlement data, cryptographic key material (ZMK, KPVV, PEK, KCVV, IMK, KAAV)Principal member, settlement bank, SEPA/instant payment processing, key provisioningYesYesmTLS via Global Accelerator
ExceetPersonalization payloads (PAN, keys)Card manufacturing and personalizationRequires verificationYesVia CBS over encrypted channel
ApataAAV values (3DS authentication data)3D Secure authentication provider for online transactionsRequires verificationYesVia CBS over encrypted channel
AWSAll CHD (encrypted at rest, clear in memory during processing)IaaS (EC2, VPC, ALB, NLB, Direct Connect), KMS, Payment Cryptography, Aurora, CloudFront, Global Accelerator, WAF/Shield, CloudTrail, IAM, S3Yes (SOC 2, PCI DSS)YesNative AWS services within VPC

Note: Carte Bancaire (CB) participates in French domestic transaction routing but Green-Got’s relationship with CB is entirely mediated through Arkéa as principal member. CB is not a direct TPSP for Green-Got and falls under Arkéa’s compliance scope.

3.10 In-Scope Locations

Facility TypeCountLocationDescription
AWS Data Center (Primary)1eu-central-1 (Frankfurt, Germany)Primary region. Hosts EC2, Aurora primary, KMS, Payment Cryptography, all networking components. 3 availability zones, 3 subnets.
AWS Data Center (Failover)1eu-west-3 (Paris, France)Failover region. Hosts Aurora replica. Switchover via Global Accelerator and CloudFront in case of primary region failure.
AWS Global Servicesus-east-1 (certificates), global (CloudFront, Global Accelerator)Some AWS services require us-east-1 (e.g., CloudFront certificates). CloudFront and Global Accelerator are globally deployed.

Green-Got operates a fully cloud-based infrastructure. There are no physical offices, data centers, or facilities where cardholder data is handled directly. Staff access the CDE remotely via Tailscale mesh VPN.

3.11 In-Scope Business Functions

FunctionDescription
Software EngineeringDevelopment, maintenance, and deployment of the Core Banking Service. Engineers have access to the CDE via Tailscale for deployment and debugging.
Infrastructure / DevOpsManagement of AWS infrastructure, security groups, VPC configuration, CI/CD pipelines, and Tailscale access policies.
Compliance / SecurityOversight of PCI DSS compliance, security monitoring (CloudTrail, Vanta), access control policies, and vendor risk management.
Executive ManagementStrategic oversight of compliance posture and risk acceptance.

4. Scope Confirmation

Based on the activities performed in this scoping review:

No new or undocumented data flows, storage locations, system components, or third-party connections were discovered during this review. No changes to the current scope are required.

5. Action Items

#ActionOwnerDue DateStatus
1Finalize and formally approve the cardholder data flow diagram (version 0.1 → 1.0)Core Banking Team / CISOBefore QSA assessmentOpen
2Verify PCI DSS assessment status for Exceet and Apata (AOC collection)ComplianceBefore QSA assessmentOpen
3Complete the cardholder data flow diagram process documentCore Banking TeamBefore QSA assessmentOpen
4Complete the cardholder network diagram process documentCore Banking TeamBefore QSA assessmentOpen
5Schedule next semi-annual scope reviewCompliance2026-10-05Open

6. Sign-Off

RoleNameSignatureDate
ReviewerEnrico — Senior Software Engineer___________________2026-04-05
Approver[CISO / Management]___________________Pending

R-2272 - Training - Security Awareness Training Completion (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

Provide security awareness program records that verify personnel attend security awareness training upon hire and at least once every 12 months.

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

R-2276 - Risk - Incident Response Records Unexpected PAN Detected

Provide incident response records of response actions that verifies when stored PAN was detected anywhere it was not expected within the past 12 months that the incident response procedures were performed.

Note: PAN detection response process in Vanta Evidence requests only documented procedures, but not records of performing such procedures.

Evidence

No incident response records exist for this request because Green-Got did not detect stored PAN in any unexpected location during the past 12 months.

The evidence request verifies that response actions are performed when stored PAN is detected where it is not expected. No such detection occurred, so there were no response actions to perform and no incident ticket, incident report, root cause analysis, secure deletion record, retrieval record, or migration record to provide for this period.

Green-Got maintains documented procedures for this scenario in PAN detection response process. That procedure activates the Incident Response Plan for detection of PAN in an unapproved location and defines reporting, triage, investigation, containment, remediation, documentation, and legal or regulatory review.

This evidence item is therefore submitted as a negative attestation for the review period: the required response procedure exists, but no unexpected PAN detection event occurred that would create response-action records.

R-2327 - Training - Acceptable Use Policy Training

Provide evidence that security awareness training includes awareness about the acceptable use of end-user technologies in accordance with Requirement 12.2.1

Evidence

Green-Got covers acceptable use of end-user technologies through its security awareness program and supporting security policies:

Approved products and assets are maintained through submitted evidence:

R-2444 - Management - Significant Change to Organizational Structure

Additional requirement for service providers only: Provide documentation, such as meeting minutes, transcripts, review notes, etc.., after any significant changes to organizational structure that allows the assessor to verify an (internal) review of the impact to PCI DSS scope and applicability of controls is performed, and results are communicated to executive management.

Evidence

This request is not applicable to the current assessment period as a separate significant-change review record.

Green-Got is undergoing its first PCI DSS assessment as a service provider. The move from operating through a third-party banking provider to operating under Green-Got’s own banking license and direct Mastercard connectivity is part of the initial scope baseline for this assessment, not a change that occurred after an established PCI DSS assessment scope was already in place.

The current PCI DSS scope baseline is documented in Scoping Exercise 2026-H1. That review identifies Green-Got’s current service-provider scope.

Future significant changes to organizational structure that affect PCI DSS scope or control applicability will be reviewed through the scope-validation process and communicated to executive management and documented here.

R-3077 - Uncategorized - Multi-Tenant Service Providers

Provide documentation maintained internally (e.g. access logs, security operations procedures, implementation/build guides, network security controls, network diagrams, etc.) that support that processes are in place for implementing controls such that each customer only has permission to access its own account data and CDE. Customers should not be able to access other customers’ environments.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Appendix A1.1.2 applies to multi-tenant service providers, requiring controls that restrict each customer to accessing only its own account data and CDE. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not allocate distinct customer CDEs or environments.

Green-Got’s customers interact with its banking service as end users. They do not receive direct access to separate environments or system resources under this control.

Remote connections disconnected ❌

Screenshot or other evidence of session termination or timeout settings for remote connections being configured to disconnect those sessions after an amount of time specified via the Access Control Policy or other applicable NIST related policies. Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% for your in-scope applications and system components devices having timeout configurations in place and operating, as randomly selected and requested by your auditor.

Remote connections monitored and encrypted

Provide screenshots or other evidence that remote connections to all applications and infrastructure are encrypted and actively monitored. Guidance: The use of TLS 1.2+ is highly recommended along with ingestion of replete logs containing authentication and authorization information regarding remote connections for your internal and external facing infrastructure. Audit Consideration(if an audit is applicable): During your audit window, be prepared to re-upload evidence for at least a 10% personnel with access to the VPN or other remote access system, as randomly selected and requested by your auditor.

Evidence

Green-Got uses Tailscale for remote access to internal applications and infrastructure. Tailscale is built on WireGuard. Tailscale documents this design in Tailscale encryption and summarizes the security model in Tailscale security.

Remote connections to internal systems therefore traverse the Tailscale mesh VPN and are encrypted in transit through the WireGuard tunnel before reaching Green-Got systems. The related Only secure methods and protocols are used for the transfer and administration of PCI systems evidence documents that internal administrative access goes through Tailscale/WireGuard and that no internal service is directly exposed to the public internet for remote administration.

Green-Got monitors operator activity through application-specific audit logs created by Green-Got systems. These audit logs record operator activity in internal systems and support attribution of actions to the operators who performed them. The User account non-repudiation evidence documents the account and session controls that support this attribution.

Green-Got stores these audit logs in ClickHouse, which acts as the central audit log store for PCI-relevant activity. The Monitoring - Log Forwarding evidence documents the central ClickHouse audit log store and backup configuration, and the Monitoring - Automated Audit Log Review evidence documents automated alert review through ClickStack / HyperDX.

These layers provide encrypted remote access through Tailscale/WireGuard, centralized storage of Green-Got-created audit logs in ClickHouse, automated review through ClickStack / HyperDX, and application-specific audit logging for operator activity.

Remote maintenance sessions secured ❌

Screenshot or other evidence that remote maintenance or other network access sessions via VPN or other remote access methods where not physically interacting with the host require MFA prior to access and that sessions terminate when not completed. Audit Consideration(if an audit is applicable): During your audit window, be prepared to re-upload evidence for at least a 10% personnel with access to the VPN or other remote access system, as randomly selected and requested by your auditor.

Remote manipulation of PAN denied by default ❌

Provide evidence of a formal DLP tool or equivalent permissions structure that prohibits copy/paste and/or relocation of PAN via remote tools unless explicitly authorized.

Rogue AP scanning

Provide evidence of regular testing or scanning that is performed on the network to identify any rogue or unauthorized access points

Evidence

Green-Got does not operate corporate-managed wireless infrastructure within environments that connect directly to the Cardholder Data Environment. Production systems are hosted entirely within AWS and accessed through secured administrative channels.

The organization does not maintain company-controlled wireless access points connected to internal networks supporting the CDE.

Roles and responsibilities for access to cardholder data and the cardholder data environment are clearly defined ❌

Evidence that shows roles, users, and privileges assigned for those with access to the CDE and CHD. Guidance: Provide screenshot of users, groups, and specified privileges to the CDE; user access should be assigned based on least privilege, default deny, and appropriate to the user’s job classification and function and should include explicit approval for which roles are allowed to see un-obfuscated PAN.

Roles and responsibilities validation completed ❌

Provide evidence that employee performance is evaluated against their roles and responsibilities at least quarterly. Reviews and evaluations must be completed by persons other than those responsible for performing the given task. This must include at minimum:

S3 backup configured for redundancy across regions (AWS)

This test verifies that AWS S3 buckets are configured with enabled cross-region replication rules to ensure data redundancy and disaster recovery readiness across different AWS regions.

Evidence

We are deactivating this Vanta test because our object storage already writes each object to two separate S3 buckets in eu-central-1 and eu-west-3.

AWS documents that Amazon S3 is designed for 99.999999999% (11 nines) of durability and stores data redundantly across a minimum of three Availability Zones by default:

Cross-region replication would duplicate the redundancy we already maintain, so this Vanta test is deactivated.

SAD is protected by issuers and those providing issuing services

Evidence that issuers and those providing issuing services have documented justification for storing SAD and protect data appropriately. Guidance: This applies only to issuers (those who issue/print credit and debit cards). If you are an issuer, provide a screenshot of policy describing justification of why SAD is stored and business justification. If you are not classified as an issuer, use the ‘Deactivate’ button and include an explanation.

Evidence

Green-Got operates as a card issuer and stores Sensitive Authentication Data (SAD) as required for legitimate issuing operations, in accordance with the exemption under PCI DSS Requirement 3.3.3.

SAD Storage Justification

SAD TypeStorageBusiness Justification
PINPersistent (PostgreSQL, encrypted)Stored to support the cardholder-facing PIN reveal feature in the Green-Got app. As the issuer, Green-Got is the authoritative source of PIN data and stores it to enable secure in-app display without requiring a network round-trip to a third party.
CVV2/CVC2Persistent (PostgreSQL, encrypted)Stored for two issuing operations: (1) in-app card details display (cardholder views full card credentials), and (2) card manufacturing — the CVV2/CVC2 is transmitted to Exceet (card manufacturer) in the personalization payload. Green-Got generates the CVV2/CVC2 during card issuance and is the authoritative source.
PVV (PIN Verification Value)Persistent (PostgreSQL, encrypted)Stored to support online PIN verification. The PVV is derived during card issuance via AWS Payment Cryptography GeneratePinData and verified during transactions using VerifyPinData. Persistence is required for transaction authorization.
CVC2 (received in CNP authorization)Not stored (ephemeral only)Received in card-not-present authorization requests (e-commerce, 3DS). Validated against the stored CVV2/CVC2 value, then discarded. Never persisted.
Track dataNot stored (ephemeral only)Processed in-memory during transaction authorization only (magstripe, for international compatibility). Never persisted.
PIN blocksNot stored (ephemeral only)Generated on-demand for Exceet manufacturing payload. Never persisted.
ARQC/ARPC, AAV cryptogramsNot stored (ephemeral only)Validated during transaction processing only. Never persisted.

Protection Measures

All persistently stored SAD is encrypted using ChaCha20-Poly1305 with KMS-generated 256-bit data keys managed via AWS KMS (envelope encryption with HSM-backed KEK). This satisfies the strong cryptography requirement of PCI DSS 3.3.3.

For the full cryptographic architecture, see 2_cryptography_key_management.md.

Secure Key Storage

Provide evidence that cryptographic keys used to encrypt or decrypt stored account data are stored securely in an approved, protected form. Acceptable storage methods include hardware security modules (HSMs), secure cryptographic devices, or strong encryption under strict access controls, as required by PCI DSS.

Evidence

Green-Got stores the keys used to protect stored account data in managed HSM-backed AWS services.

In summary, the cryptographic keys used to encrypt or decrypt stored account data are protected as follows:

Secure Remote Access

Database

Non-console administrative (remote admin) access encryption Provide screenshots showing secure remote access to at least 3 databases.

Evidence

Remote database access is also secured via Tailscale. Our servers act as a Subnet Router proxying access to our database via a secure Wireguard tunnel. Database access is managed with AWS IAM Database authentication

Hypervisors

Non-console administrative (remote admin) access encryption Provide screenshots showing secure remote access to 3 hypervisors or containers(or all if fewer than 3) (e.g. TLS 1.2).

Evidence

We use AWS ECS as our hypervisor. The underlying system cannot be accessed by us and it can only be controlled through the AWS dashboard or APIs. We run AWS ECS on our own AWS EC2 instances. These instances also don’t allow remote access. Remote access via SSH is disabled by default on our containers as well and is only enabled when absultly needed. This access is managed with Tailscale Access Controls and the connection would happen though a secure Wireguard tunnel via Tailscale SSH

Mainframe

Non-console administrative (remote admin) access encryption Provide screenshots showing secure remote access to the mainframe (e.g. SSH).

Evidence

We don’t have a Mainframe. Access to our servers is described in Secure Remote Access - Hypervisors

Network & CSP

Non-console administrative access encryption Provide one screenshot showing secure remote access (e.g. TLS 1.2) to each of the different types of network devices (e.g. cloud service providers, databases, containers, firewalls, routers, switch, IDS/IPS, wireless devices and appliances).

Evidence

Everything mentioned above is managed via the AWS Console or AWS APIs except for databases and containers. Access to databases and containers is explained in Secure Remote Access - Hypervisors and Secure Remote Access - Database.

For everything else the responsibility for security falls on AWS to secure their APIs. Our management of access to AWS resources is explained in Access and IAM Credentials Rotated.

POS OS

Non-console administrative (remote admin) access encryption Provide screenshots showing secure access to at least 3 (or all if fewer than 3) different POS/POI devices.

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

Unix/Linux

Non-console administrative (remote admin) access encryption Provide screenshots showing secure access to at least 3 (or all if fewer than 3) different types of OS.

Evidence

The only usecase in which we use remote Unix machines that are not already covered in Secure Remote Access - Hypervisors is for CICD or remote development purposes. In this case as well the incoming traffic is limited to return traffic and the machine is only access via Tailscale / Tailscale SSH

Windows

Non-console administrative (remote admin) access encryption 1-) Provide screenshots showing secure access to at least 3 (or all if fewer than 3) different types of OS. 2-) If Windows Remote Desktop is used, provide evidence (like screenshot) showing that it is configured to use “high encryption” for remote administration..

Evidence

There are no windows machines used in our production environment, we use a windows machine to run the mastercard simulator but this is only used for non-production environments.

Secure configuration baselines developed ❌

Provide evidence of documented configuration baselines based on industry standards such as CIS, NIST 800 series, NIST STIGs, etc.) for all in-scope systems such as:

  1. Operating Systems
  2. Cloud Infrastructure and Services
  3. Servers or Compute Instances
  4. Endpoint Software
  5. Network Infrastructure (Firewalls, VPCs, Security Groups)
  6. Databases

Acceptable evidence can include:

Secure engineering principles defined

Please upload a completed copy of a Secure Engineering Principles & Planning document. This document should speak to what your organization considers its guiding principles for secure engineering and operate as a north star reference when considering engineering challenges. For ideas on what to consider it is recommended that you review NIST SP 800-160v1r1 also known as “Engineering Trustworthy Secure Systems” from NIST. A template can be found here: Google docs template

Evidence

Green-Got defines its secure engineering principles in the Secure Development Policy. The policy is owned by Fabien Huet and was effective and last reviewed on March 9, 2026.

The policy establishes secure system engineering principles for Green-Got applications and information systems that are business critical or process, store, or transmit Confidential data. It applies to internal and external engineers and developers of Green-Got software and infrastructure.

Green-Got applies the following secure-by-design principles:

PrincipleApplication
Minimize attack surface areaEngineering work reduces exposed interfaces, services, permissions, and entry points to the minimum needed for the system purpose.
Establish secure defaultsSystems are configured with restrictive defaults and require explicit approval or configuration for elevated access or broader exposure.
Least privilegeUsers, services, and systems operate with the minimum access required for their assigned function.
Defense in depthSecurity controls are layered across application, infrastructure, identity, monitoring, and operational processes.
Fail securelyAuthentication, authorization, validation, and processing failures result in a denied or protected state.
Do not trust servicesIntegrations and service boundaries are treated as untrusted unless authenticated, authorized, and validated.
Avoid security by obscuritySecurity decisions rely on explicit controls and reviewable configurations instead of hidden implementation details.
Keep security simpleEngineering designs favor clear, maintainable controls that teams understand and operate consistently.
Fix security issues correctlySecurity remediation addresses the underlying weakness and includes validation that the issue is resolved.

Green-Got also applies the following privacy-by-design principles:

PrincipleApplication
Proactive not reactive; preventative not remedialPrivacy and security risks are addressed during design and implementation rather than only after incidents.
Privacy as the default settingPersonal data handling defaults to limited collection, limited access, and controlled processing.
Privacy embedded into designData protection is part of system design, engineering review, and implementation decisions.
Full functionality, positive-sumSecurity and privacy controls are designed to support business functionality while protecting users and data.
End-to-end security, full lifecycle protectionData protection requirements apply from collection through processing, storage, retention, and deletion.
Visibility and transparencySecurity and privacy controls are documented and reviewable through policies, procedures, and engineering records.
Respect for user privacyEngineering decisions account for user privacy, data minimization, and controlled data access.

Secure password storage and transmission ❌

Screenshot, vendor documentation or other evidence that passwords are stored and transmitted securely using industry accepted methods such as salting and hashing, encryption, rate limiting (required for OFDSS), etc. Including evidence that first time passwords or temporary passwords force a password change when used. Note: For more information see https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines

Security Awareness Program Documentation

Provide documentation demonstrating that your security awareness program is actively maintained, reviewed annually, and uses multiple methods to educate personnel on protecting cardholder data. The program should be updated as needed to address new threats and evolving security practices, and must include diverse communication methods (e.g., trainings, phishing simulations, internal messages) to ensure broad and ongoing engagement.

Evidence

Green-Got manages the security awareness program through Vanta and uses multiple methods to educate personnel on protecting cardholder data:

Green-Got maintains the Riot phishing campaign dashboard as supporting evidence for phishing simulation coverage and reporting performance.

Riot phishing campaign dashboard

Green-Got reviews the program annually as part of the PCI DSS compliance cycle to verify that assigned trainings remain appropriate and that completion rates meet requirements. Related submitted evidence documents the assigned Social engineering training and Insider threat training. The training content available by framework is documented in Vanta’s training video access reference.

Security Awareness Training

Provide current materials used in your organization’s security awareness program to educate personnel about their responsibilities for protecting sensitive data and supporting information security policies. This evidence should also demonstrate that the training is aligned with your organization’s security policies and is delivered at least annually.

Evidence

Green-Got delivers security awareness training through Vanta’s built-in General security awareness module, assigned to all personnel via the Employees group. This training educates personnel on their responsibilities for protecting sensitive data — including cardholder data — and is aligned with Green-Got’s information security policies.

Training is delivered at least annually. Individual completion status is tracked in Vanta People. The training content available by framework is documented in Vanta’s training video access reference.

Security awareness training completion

Provide evidence of recurring security awareness training completion by your employees. Note: Provide this only if you’re NOT using Vanta’s security awareness training modules. A template can be found here: Google docs template

Evidence

This document is not applicable. Green-Got uses Vanta’s built-in security awareness training modules, assigned via Vanta People Management groups. Training completion is tracked automatically per person in Vanta People and does not require a separate bulk completion upload. Training content details are listed in Vanta’s training video access reference.

Security gap population ❌

Provide a list of all security control failures/gaps related to any security process and/or control that have occurred within the review period.

Evidence

Security issues assigned priorities

This test verifies that all open issues labeled as security in your task tracking tools have assigned priorities.

Evidence

We are deactivating this Vanta test because it is not used to satisfy PCI DSS requirements.

Our current focus is PCI DSS scope. Also Linear does not support scoping this test to the relevant teams, so issues labeled as security by other teams also appear here even though they are outside the PCI DSS scope, which adds noise to the results.

Sensitive Authentication Data (SAD) is not stored post authorization ❌

Evidence that SAD is not stored post-authorization. Guidance: Provide dataflow diagrams or vendor documentation demonstrating that SAD is never stored post-authorization. SAD includes 3/4 digit CVV/CID codes, track data, mag stripe data, PIN and PIN block, or chip data. If no SAD is captured or stored, use the ‘Deactivate’ button and include an explanation.

Sensitive data tokenized or hashed ⏱️

Provide documentation regarding the hashing method used to render Primary Account Number (PAN) unreadable, including the vendor, type of system/process, and encryption algorithms (as applicable) to verify that the hashing method results in keyed cryptographic hashes of the entire PAN.

Evidence

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-04-21
Last Updated2026-05-01
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material PAN lookup control change

Green-Got renders stored PAN unreadable using application-side envelope encryption, as documented in PAN is rendered unreadable anywhere it is stored and PAN Encryption or Obfuscation - Database. When a PAN is created, the PAN is automatically encrypted before database storage. In addition to encrypted PAN storage, Green-Got creates keyed HMAC-SHA256 lookup values of the full PAN for deterministic lookup without decrypting stored PAN.

The keyed HMAC lookup value is an internal deterministic lookup value generated from the entire PAN. It is not exposed or used as a public identifier. The active and secondary PAN lookup HMAC keys are stored as protected AWS Systems Manager Parameter Store SecureString values and loaded into application memory during process startup. When an external authorization message, including from Mastercard, contains a clear PAN, Green-Got computes the keyed HMAC lookup value over that PAN with the loaded lookup key set and compares it with the stored keyed HMAC lookup values to identify the matching encrypted PAN record. This is a deterministic recomputation of the keyed HMAC value; it is not a reconstruction of the PAN from the keyed HMAC lookup value. The clear PAN remains protected by the storage encryption controls and is not stored in clear text.

Hashing Method

AttributeImplementation
VendorAWS Systems Manager Parameter Store, AWS KMS, and the Green-Got application runtime
Type of system/processApplication-side keyed HMAC-SHA256 PAN lookup value generation and verification using startup-loaded key material
PAN inputEntire PAN
Cryptographic methodKeyed HMAC-SHA256
Key strength256 bits
Key typeApplication-managed HMAC secret stored as AWS Systems Manager Parameter Store SecureString values
Production storageProduction AWS account active and secondary PAN lookup HMAC SecureString values
Non-production storageNon-production AWS account active and secondary PAN lookup HMAC SecureString values
PurposePAN lookup and matching without decrypting stored PAN
Cryptoperiod12 months from activation date for the active production PAN lookup HMAC key
Rotation windowMaximum 30 calendar days from secondary-key publication to previous-key retirement, including lookup slot backfill and verification
Access controlIAM-authorized access to the protected Parameter Store values and their AWS KMS decryption path is required to load the PAN lookup HMAC keys into the application
Display and disclosureThe keyed HMAC lookup value is not displayed to operators or customers and is not returned in customer-facing APIs
Rotation modelTwo dated PAN lookup slots are maintained in the PAN inventory; the rotation worker writes the new keyed HMAC lookup value into the oldest slot

Production and non-production PAN lookup HMAC keys are distinct secrets stored in separate AWS accounts and separate Parameter Store paths. The same PAN lookup HMAC key material is not reused across production, staging, local-development, or test environments.

Internal Lookup Flow

The PAN lookup flow operates as follows:

  1. A PAN is created in the CDE.
  2. The application generates a keyed HMAC-SHA256 lookup value from the clear PAN using the active startup-loaded PAN lookup HMAC key.
  3. The application encrypts the PAN before storing it in the database.
  4. The encrypted PAN envelope and the keyed HMAC lookup value are stored with the PAN record.
  5. When a clear PAN is later received in an authorized processing flow, Green-Got computes the keyed HMAC-SHA256 lookup value again using the active startup-loaded key and, during a rotation window, also computes the keyed HMAC-SHA256 lookup value using the secondary startup-loaded key.
  6. During normal operation, the computed keyed HMAC lookup value is compared against the stored lookup slots. During a rotation window, the lookup query compares the values computed with the active and secondary keys against both stored keyed HMAC lookup slots, and a match in either slot identifies the encrypted PAN record without decrypting all stored PAN values.

HMAC Key Rotation

Green-Got rotates production PAN lookup HMAC keys on a 12-month cryptoperiod. The maximum rotation window is 30 calendar days from secondary-key publication to previous-key retirement.

HMAC key rotation is performed as a controlled change. The rotation window covers both the PAN inventory backfill and the verification activities required before the previous key is retired. The change identifies the current key, the new key, the rotation window, the verification plan, and the owner responsible for completion.

During the rotation window, Green-Got loads the active and secondary PAN lookup HMAC keys, updates the PAN inventory with the new keyed HMAC lookup values, tracks update failures, and verifies completion before the new key becomes the only key retained in memory. PAN lookups remain available during rotation because the application computes lookup values with both loaded keys and matches against both stored lookup slots.

The detailed PAN inventory slot model is documented in PAN Handling.

The PAN lookup HMAC secret inventory is documented in Encryption Key Inventory Maintained and Key and certificate inventory maintained. The inventory identifies the production and non-production active and secondary lookup secrets, their storage location in AWS Systems Manager Parameter Store, ownership, and rotation notes.

The retained PAN representations and reconstruction controls are documented in Prevention of PAN Reconstruction. Green-Got does not store an unkeyed hash of PAN. The retained representations are encrypted PAN, keyed HMAC-SHA256 lookup value, masked PAN for display and support workflows, and external correlation tokens that are not derived from PAN.

The keyed HMAC lookup value by itself does not allow PAN reconstruction. Producing or verifying the value requires the original PAN and authorized access to the protected PAN lookup HMAC key material loaded from AWS Systems Manager Parameter Store. The masked PAN does not contain enough digits to reconstruct the full PAN, and correlating a masked value with the keyed HMAC lookup value does not reveal the missing digits or decrypt the encrypted PAN envelope.

External Correlation Tokens

The PAN keyed HMAC lookup value is only used internally by Green-Got and is not shared with external parties. When Green-Got needs an external correlation identifier, for example with Apata for 3DS card-link and transaction-correlation flows, Green-Got uses a separate unique identifier such as apata_correlation_id. That identifier is unique to the card, is not derived from PAN, and is used instead of PAN-derived identifiers for external correlation.

Change History

VersionDateAuthorChanges
1.02026-04-23julian@green-got.comDocument PAN lookup HMAC hashing and deterministic lookup controls.
1.12026-04-30etienne@green-got.com, julian@green-got.comAlign PAN lookup HMAC controls with application-managed HMAC secrets stored in AWS Systems Manager Parameter Store, coordinated redeploy rotation, and dual-slot backfill controls.
1.22026-05-01julian@green-got.comClarify timestamp-based dual-slot HMAC rotation and use the rotation window for backfill and verification.

Service Provider offer secure protocol option. ❌

System configurations and supporting documentation verify the service provider offers a secure protocol option for its service.

Service Providers maintain documented cryptographic architecture ❌

Service provider maintains documented description of the cryptographic architecture for protection of customer data. Guidance: If encryption is used to protect CHD, document encryption procedures that include:

Service Providers provide guidance to customers about key management

Documentation created by Service Providers describing customer requirements for securely transmitting, storing, and updating shared keys. Guidance: This applies only to service providers who share keys with their customers (not typical). If no keys are shared with customers, use the ‘Deactivate’ button and include an explanation.

Evidence

This requirement is not applicable to Green-Got.

Green-Got operates as a banking institution providing financial services directly to retail consumers and business customers. Green-Got does not provide banking-as-a-service, payment infrastructure, or any other service to other financial institutions or service providers. All of Green-Got’s customers are end-users of its banking services.

Green-Got does not share, distribute, or exchange cryptographic keys with its customers. No shared keys exist between Green-Got and its customers for the transmission, storage, or processing of cardholder data. Customers interact with Green-Got’s banking services through standard authenticated interfaces and do not participate in any key management processes.

Service account protections - configuration

Provide evidence that all service accounts that are capable of a user-interactive login have the following protections enabled: • Interactive use is prevented unless needed for an exceptional circumstance. • Interactive use is limited to the time needed for the exceptional circumstance. • Business justification for interactive use is officially documented. • Interactive use is explicitly approved by management. • Individual user identity is confirmed before access to account is granted. • Every action taken is attributable to an individual user.

Evidence

Green-Got enforces a strict separation between human interactive access and service access. No service account in the environment is enabled for interactive login.

Identity model

Applicability of the required controls

Since no service account has interactive login capability:

Shared Responsibility Matrix ❌

Provide a documented Shared Responsibility Matrix (SRM) that outlines how PCI DSS requirements are allocated between your organization and any third parties, such as cloud service providers or payment processors. The SRM should clearly define which party is responsible, accountable, or supports each applicable PCI DSS requirement, based on your service delivery model (e.g., IaaS, SaaS, PaaS). This matrix helps demonstrate how full PCI DSS coverage is achieved across shared environments and supports scoping and compliance validation. Additional guidance can be found here: PCI DSS Third-Party Security Assurance

Shared account protections

Evidence

All interactive access to CHD-scoped systems and core business systems is assigned to individually identified users. Green-Got does not use shared human accounts in these environments.

All in-scope systems enforce individual account provisioning. This includes, among others, the back office, AWS, PostgreSQL, SigNoz, and ClickHouse — each of which requires named individual user accounts.

As a result, every action performed in these systems is attributable to a single identified user. No shared-account access exists for normal operations, no exceptional-use windows for shared accounts are defined, and no business justifications or management approvals for shared-account use have been issued.

We discourages sharing account credentials or allowing others to use one’s account. Outside PCI scope, Green-Got does not centrally audit every third-party service for shared-account usage, so no blanket assertion is made for out-of-scope contexts.

Social engineering training

Provide training that addresses common social engineering, phishing, and related attacks - how to identify phishing and other social engineering attacks, how to react to suspected phishing and social engineering, and where and how to report suspected phishing and social engineering activity.

Evidence

Green-Got assigns Vanta’s built-in Social engineering training to all personnel through the Employees group. This training covers:

Training is delivered annually. Individual completion is tracked in Vanta People. Training content details are listed in Vanta’s training video access reference.

Software Vulnerabilities

Provide documentation demonstrating the tracking and remediation of vulnerabilities in custom, open-source, and third-party components. This includes dependency analysis, open-source component tracking tools, and the pre-merge checks used to prevent vulnerable dependencies from being introduced.

Evidence

Green-Got uses a layered set of tools and review processes to identify and remediate vulnerabilities across custom-developed code, open-source dependencies, and third-party components. The formal vulnerability management procedure governing this process is documented in Vulnerability Management Procedure.

Custom-developed code review and pre-merge checks

Custom-developed code changes are reviewed through the standard pull-request process before merge. The custom CI pipeline runs pre-merge build, test, format, lint, deployment, and dependency checks depending on the affected files. Application defects identified during review or CI are remediated before merge or tracked through the vulnerability management process when follow-up work is required.

Software Composition Analysis (SCA) and dependency tracking

Two controls provide dependency vulnerability detection:

Remediation of dependency vulnerabilities is automated through a production process:

  1. Daily security alert remediation — queries open Dependabot alerts and creates pull requests to resolve security vulnerabilities.

Remediation metrics for high-severity dependency vulnerabilities are documented in High and critical vulnerability rescan, showing an average resolution time of approximately 2 days across all 9 high-severity alerts.

Container and infrastructure scanning

Open-source component inventory

All application code lives in a single monorepo (GitHub Repository). The Software Bill of Materials is maintained through Cargo.lock and Dockerfile, as documented in Custom developed software inventory.

Vulnerability intelligence sources

Active monitoring of trusted vulnerability intelligence sources is documented in Vulnerability Intelligence Sources.

Strong encryption used msg ❌

Supplier/vendor agreements

Provide a recent agreement signed with one of your service providers that outlines the division of cybersecurity and technology risk management responsibilities. Guidance: This may include service agreements with vendors such as:

Evidence

Green-Got’s primary cloud infrastructure provider is Amazon Web Services (AWS). As a large-scale cloud provider, AWS does not negotiate individual customer contracts for shared responsibility. Instead, AWS publishes standardized agreements and responsibility frameworks that govern the division of security responsibilities:

These documents collectively define the division of cybersecurity and technology risk management responsibilities between Green-Got and AWS, covering security controls, incident response, data protection, and system availability.

System Change Population ❌

Provide a listing of all recent (past 12 months) changes or updates to any system components (including O.S., firmware, and/or system config updates) residing within the in-scope environment. This list can include all associated change control ticket numbers for all such changes that the assessor can sample from when testing.

Evidence

System Configuration

Database Sample ❌

System components review samples Provide evidence (like script/command output or screenshots) of the following configuration settings for each of the databases below: 1-) Data repository name 2-) Name and version of the data base software used 3-) List of all database users (PCI 2.2.2; 8.1; 8.2.2) 4-) Last security patch installed (PCI 6.2) 5-) Local password configuration settings that show: • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9) • Change user passwords at least every 90 days (PCI 8.2.4). • Passwords history of at least 4 (PCI 8.3.7). • Lock out after not more than six attempts (PCI 8.3.4). • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4). • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8). 6-) Audit log settings 7-) Vendor security patch list. This list should include a link to the vendor’s website which shows the latest patches available and should reconcile with point #4 above. Guidance: • Pending inventory with total system population (network devices and appliances) - To be identified by QSA. • Please pull at least one example to familiarize yourself with the process - your QSA will re-sample and provide clarity during your audit window

Evidence

Database configuration also happens via Infrastrucure as Code

Endpoint Sample 📬

Provide screenshots of configuration settings from a sample of endpoint devices (e.g., desktops, laptops, servers, smartphones, or BYOD) that can connect to both trusted and untrusted networks. These screenshots must demonstrate that endpoint security controls—such as antivirus/antimalware, EDR, and software firewalls—are active and cannot be disabled or modified by the end user.

Hypervisors & Containers Sample ❌

System components review samples (High Priority) For the sampled systems, provide screenshots and/or running configuration files clearly showing the following configuration parameters: 1-) Host name 2-) OS name and version 3-) List of all local user accounts (PCI 2.2.2; 8.1; 8.2.2) 4-) Last security patch installed (PCI 6.2) 5-) Local password configuration settings that show: • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9) • Passwords history of at least 4 (PCI 8.3.7). • Lock out after not more than six attempts (PCI 8.3.4). • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4). • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8). 6-) Audit log settings (PCI 10.2). 7-) NTP settings (PCI 10.4). 8-) Centralized authentication settings (e.g. TACACS) (PCI 8.x) 9-) Community strings (PCI 2.x) 10-) Vendor security patch list. This list should include a link to the vendor’s website which shows the latest patches available and should reconcile with point #4 above for each device type. Guidance: • Pending inventory with total system population (network devices and appliances) - To be identified by QSA. • Please pull at least one example to familiarize yourself with the process - your QSA will re-sample during your audit window

Network CSP Sample ❌

System Components Review Samples

For the sampled systems, provide screenshots and/or running configuration files clearly showing the following configuration parameters:

  1. Tool name - Screenshot of dashboard for network and services administration
  2. List of network segments
  3. ACL and firewall rules in place for network segment(s)
  4. List of all user accounts with access to administrate segments (PCI 2.2.2; 8.1; 8.2.2)
  5. Password or authentication configuration settings that show:
    • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6)
    • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9)
    • Change user passwords at least every 90 days (PCI 8.2.4)
    • Passwords history of at least 4 (PCI 8.3.7)
    • Lock out after not more than six attempts (PCI 8.3.4)
    • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4)
    • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8)
    • Keys or certificates used in place of or as part of authentication
  6. Audit log settings (PCI 10.2)
  7. Centralized authentication settings (PCI 8.x)

Guidance

Evidence

  1. We are using Infrastructure as code to manage this. The tool is called Pulumi but there is no dashboard
  2. This is described in
  3. Part of Infrastructure as code
  4. Part of Infrastrucure as code and controlled via Tailscale Grants
  5. There are no passwords involved in this process
  6. Infrastructure as code follows the same change management procedure as Change Management Procedures
  7. Infrastructure as code changes are applied by CI once change has been approved or manually by getting permissions though the TEAM process

POS OS Sample

System components review samples (High Priority) For the sampled systems, provide script/command output or screenshots clearly showing the following configuration parameters: 1-) Host name 2-) OS name and version 3-) IP address 4-) List of all local user accounts (PCI 2.2.2; 8.1; 8.2.2) 5-) Last security patch installed (PCI 6.2) 6-) List of all processes/services running (PCI 2.x; 5.x; 11.5) 7-) Local password configuration settings that show: • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9) • Passwords history of at least 4 (PCI 8.3.7). • Lock out after not more than six attempts (PCI 8.3.4). • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4). • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8). 8-) Audit log settings (PCI 10.2). 9-) NTP settings (PCI 10.4). 10-) Vendor security patch list. This list should include a link to the vendor’s website which shows the latest patches available and should reconcile with point #5 above. 11-) Specify the name of the service(s) running for the anti-virus solution (if any). These services must be found in the output of point #6 above. 12-) Specify the name of the service(s) running for the File Integrity Monitoring (FIM) solution. These services must be found in the output of point #6 above. Guidance: • Pending inventory with total system population (network devices and appliances) - To be identified by QSA. • Please pull at least one example to familiarize yourself with the process - your QSA will re-sample and provide clarity during your audit window

Reason for deactivation

We do not operate any point-of-sale (POS) or point-of-interaction (POI) devices.

Unix/Linux Sample ❌

System components review samples (High Priority) For the sampled systems, provide script/command output or screenshots clearly showing the following configuration parameters: 1-) Host name 2-) OS name and version 3-) IP address 4-) List of all local user accounts (PCI 2.2.2; 8.1; 8.2.2) 5-) List of all administrator accounts 6-) Last security patch installed (PCI 6.2) 7-) List of all processes/services running (PCI 2.x; 5.x; 11.5) 8-) List of listening and established connections 9-) Local password configuration settings that show: • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9) • Passwords history of at least 4 (PCI 8.3.7). • Lock out after not more than six attempts (PCI 8.3.4). • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4). • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8). 10-) Audit log settings (PCI 10.2). 11-) NTP settings (PCI 10.4). 12-) Vendor security patch list. This list should include a link to the vendor’s website which shows the latest patches available and should reconcile with point #5 above. 13-) Specify the name of the service(s) running for the anti-virus solution. These services must be found in the output of point #6 above. 14-) Specify the name of the service(s) running for the File Integrity Monitoring (FIM) solution. These services must be found in the output of point #6 above. Guidance: • Pending inventory with total system population (network devices and appliances) - To be identified by QSA. • Please pull at least one example to familiarize yourself with the process - your QSA will re-sample and provide clarity during your audit window

Windows Sample ⏱️

System components review samples For the sampled systems, provide script/command output or screenshots clearly showing the following configuration parameters: 1-) Host name 2-) OS name and version 3-) IP address 4-) List of all local user accounts (PCI 2.2.2; 8.1; 8.2.2) 5-) List of all administrator accounts 6-) Last security patch installed (PCI 6.2) 7-) List of all processes/services running (PCI 2.x; 5.x; 11.5) 8-) List of listening and established connections 9-) Local password configuration settings that show: • Minimum password length of at least twelve characters where technically possible and 8 where not (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters (PCI 8.3.6) • Passwords containing both numeric and alphabetic characters if only username and password are used (PCI 8.3.9) • Passwords history of at least 4 (PCI 8.3.7). • Lock out after not more than six attempts (PCI 8.3.4). • Lockout duration of 30 minutes or until administrator enables the user ID (PCI 8.3.4). • Re-authentication for idle session of more than 15 minutes (PCI 8.2.8). 10-) Audit log settings (PCI 10.2). 11-) NTP settings (PCI 10.4). 12-) Vendor security patch list. This list should include a link to the vendor’s website which shows the latest patches available and should reconcile with point #5 above. 13-) Specify the name of the service(s) running for the anti-virus solution. These services must be found in the output of point #6 above. 14-) Specify the name of the service(s) running for the File Integrity Monitoring (FIM) solution. These services must be found in the output of point #6 above. Guidance: • Pending inventory with total system population (network devices and appliances) - To be identified by QSA. Guidance: • Please pull at least one example to familiarize yourself with the process - your QSA will re-sample and provide clarity during your audit window

Evidence

We don’t use any Windows machines for production environments.

System Configuration - Security Impacting Admin Workstation Hardening (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

System components review samples For the sampled systems, provide screenshots and/or running configuration files clearly showing the following configuration parameters: 1-) List of all user accounts with access to administrate segments (PCI 2.2.2; 8.1; 8.2.2) 2-) Password or authentication configuration settings that show:

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

System Configuration - Security Impacting In-scope System Console (SAMPLE) 📬

SAMPLE Task: QSA to select randomized sample set from total populations (P-##) provided -

System components review samples For the sampled systems, provide screenshots and/or running configuration files clearly showing the following configuration parameters: 1-) List of all user accounts with access 2-) Password or authentication configuration settings that show:

3-) Centralized authentication settings (PCI 8.x) (if applicable)

Evidence

The full population for this request is maintained internally. Green-Got is waiting for the QSA to identify the final sample set during the audit window before assembling the evidence package for this request.

One representative example may be prepared ahead of time to validate the retrieval process. The formal submission for this request will follow the auditor-selected sample.

Targeted Risk Assessment for TLS supporting Documentation

Provide supporting documentation related to TLS Targeted Risk Analysis (TRA). Note: This document request will mark as completed when anything is uploaded. Ensure that all supporting documentation for all applicable TRAs is included prior to uploading to ensure full coverage.

Evidence

Green-Got is not currently using the PCI DSS customized approach for any requirement related to TLS. No TLS-specific Targeted Risk Analysis (TRA) has been performed because none is required under the defined approach.

All TLS configurations enforce TLS 1.3 in accordance with PCI DSS defined-approach requirements. As no customized-approach controls matrix or targeted risk analysis package for TLS exists, there is no supporting documentation to provide.

Requirement 12.3.2 as it relates to TLS is therefore not currently applicable, and no TLS TRA supporting documentation is produced.

Targeted Risk Assessment supporting documentation

Provide supporting documentation related to each Targeted Risk Analysis (TRA). Note: This document request will mark as completed when anything is uploaded. Ensure that all supporting documentation for all applicable TRAs is included prior to uploading to ensure full coverage.

Evidence

Green-Got is not currently using the PCI DSS customized approach for any active requirement. No Targeted Risk Analysis (TRA) has been performed because none is required under the defined approach.

As no customized-approach controls matrix or targeted risk analysis package exists, there is no supporting documentation to provide.

Requirement 12.3.2 is therefore not currently applicable, and no TRA supporting documentation is produced.

Targeted Risk Assessment-TLS completed

Provide evidence that for any requirements using the customized approach, a Targeted Risk Analysis (TRA), for TLS has been completed.

Evidence

Green-Got is not currently using the PCI DSS customized approach for any requirement related to TLS configuration or cryptographic protocols.

All TLS configurations follow the PCI DSS defined approach and enforce TLS 1.3. No customized-approach controls matrix or targeted risk analysis package for TLS exists.

Requirement 12.3.2 as it relates to TLS is therefore not currently applicable.

Targeted Risk Assessments Completed

Provide evidence that for any requirements using the customized approach, a Targeted Risk Analysis (TRA) has been completed.

Evidence

Green-Got does not use the PCI DSS customized approach for any active PCI DSS requirement.

All active PCI DSS requirements are assessed using the defined approach.

No customized-approach controls matrix or Targeted Risk Analysis (TRA) package exists. PCI DSS v4.0.1 Requirement 12.3.2 is not currently applicable.

Technology Review and EOL Remediation Plan

Provide documentation showing that hardware and software technologies in use are reviewed at least annually to ensure they continue to receive vendor security updates, support PCI DSS compliance, and are monitored for industry announcements such as end-of-life (EOL) declarations. Include any remediation or replacement plans for outdated or soon-to-be unsupported technologies, with sign-off from senior management.

Evidence

An annual recurring issue is configured in the PCI DSS Linear team for this control. The recurring issue entry is named Perform and document PCI DSS technology review and EOL remediation plan and is scheduled Yearly, with the next due date shown as Apr 15.

This recurring issue documents the scheduled annual review cadence for hardware and software technologies in scope and records the recurring work item used to track the review and any resulting remediation planning. The recurring issue is visible in the PCI DSS team recurring issues view alongside the incident response plan test and access review recurring issues.

The recurring issue is tracked in Linear at PCI-6: Perform and document PCI DSS technology review and EOL remediation.

The recurring issue configuration is captured in the screenshot below:

Linear recurring issue for technology review and EOL remediation plan

Test of incident response plan

Provide evidence demonstrating periodic tests of the incident response plan, such as tabletop exercises or simulated incident drills. The evidence should confirm that testing is scheduled, executed, and reviewed. Acceptable evidence can include:

Evidence

Green-Got tracks and schedules the annual incident response plan test in the PCI DSS Linear team. The recurring issue establishes the yearly cadence and tracks the work needed to perform and document the test.

The recurring issue records the scheduling component of the evidence request and keeps incident response plan testing on the PCI DSS compliance calendar.

The supporting screenshot shows the annual Linear recurrence used for this control.

Linear recurring issue for incident response plan test

Third parties with access to the environment are monitored while connected ⏱️

Evidence showing that third party access is enabled only when needed, disabled when not in use, and vendor activity is monitored. Guidance: Provide screenshots of user list showing no vendor accounts active. If applicable, provide screenshot of logging sample showing vendor activity is monitored. If no third parties/external vendors access the CDE, use the ‘Deactivate’ button and include an explanation.

Evidence

No third-party vendors have remote access to Green-Got’s Cardholder Data Environment (CDE). All system components within the CDE are managed exclusively by Green-Got personnel. No vendor accounts exist in the CDE’s authentication systems (AWS IAM Identity Center, Tailscale, or any infrastructure component).

Green-Got’s infrastructure is hosted on AWS, which operates under a shared responsibility model. AWS does not have logical access to Green-Got’s CDE at the application or data layer. All administrative access to the CDE is restricted to Green-Got employees through centralized identity management with enforced multi-factor authentication.

Because no third-party accounts exist for remote access to the CDE, PCI DSS Requirement 8.2.7 is not applicable. This control has been deactivated in Vanta accordingly.

Time Synchronization Management - External time sources 📬

For a random sample of at least 3 (or all if fewer than 3) designated central time servers or sources used (that receive time synchronization from external sources), provide screenshot of the configuration showing the external time sources used for time synchronization. Cloud Platforms usually do this by default, so a screenshot of your default time settings or configuration should work. Note: the sample must include all types of designated central time servers used (e.g. routers, time appliances, domain controllers, etc).

Time Synchronization Management - Protection of Time Data 📬

For a random sample of at least 3 (or all if fewer than 3) designated central time servers or sources used: 1-) Provide evidence (like a screenshot) clearly showing the configuration settings to log changes to time settings. 2-) Provide a log entry resulting after changes made to time settings. Note: the sample must include all types of designated central time servers used (e.g. cloud service providers, routers, time appliances, domain controllers, etc).

Time clocks are synchronized ❌

Provide screenshots or other evidence of an industry accepted centralized time source in use and synchronized for all in-scope systems not technically integrated with Vanta. Guidance: Attempting to cross-reference logs from hosts and applications across multiple timezones with potentially unreliable time-tracking is untenable at scale. The intention behind this is to demonstrate that all systems are synchronized to one time source and that source is trusted to be accurate. Some options for accurate time synchronization include the use of:

  1. NIST: time.nist.gov
  2. Microsoft: time.windows.com
  3. Google: time.google.com
  4. Apple: time.apple.com

Unauthenticated vulnerability scan systems are clearly identified ❌

Evidence that any systems that are not able to accept credentials for authenticated vulnerability scanning are formally identified.

Unique Assignment of Authentication Factors ❌

Provide documentation confirming that all physical or logical authentication factors (e.g., smart cards, hardware tokens, certificates) are uniquely assigned to individual users and not shared.

Evidence

1. Authentication Factor in Use

The Green-Got back office exclusively uses WebAuthn/FIDO2 passkeys as its authentication factor. Passkeys are cryptographic credentials bound to the operator’s mobile device (smartphone).

The WebAuthn configuration enforces the following policies at registration time:

PolicyValueEffect
Authenticator AttachmentCrossPlatformRequires an external mobile device, not a browser-embedded credential
User VerificationRequiredThe authenticator verifies the user’s identity (biometric or PIN) before signing
Resident KeyRequiredThe passkey is stored on the operator’s mobile device
Credential HintsHybridCross-device flow via the operator’s mobile phone (QR code scan)
Allowed TransportsHybrid onlyUSB and Internal transports are explicitly filtered out, enforcing mobile-only authentication

These settings ensure that each authentication factor is:

[Screenshot placeholder: WebAuthn registration prompt in the back office showing mobile device QR code scan]


2. Unique Assignment — System Configuration Controls

2.1 Database Unique Constraint

A partial unique index on the bo_passkey table enforces that each operator has at most one active or pending passkey at any time:

CREATE UNIQUE INDEX idx_bo_passkey_operator_non_deleted_unique
  ON bo_passkey(operator_id)
  WHERE status IN ('Pending', 'Active');

This constraint prevents:

If an operator needs to replace their passkey (e.g., lost or changed phone), the existing passkey is first deleted by an admin before a new one is registered.

2.2 Passkey Data Model

Each passkey record in the bo_passkey table stores:

FieldDescription
idUnique passkey identifier
operator_idForeign key to the operator (user) who owns this passkey
credentialThe WebAuthn credential data (public key, counter, etc.)
credential_idDerived from the WebAuthn credential, unique per authenticator
deviceDescription of the authenticator device
statusOne of: Pending, Active, Rejected, Deleted
last_used_atTimestamp of last successful authentication

The WebAuthn credential is cryptographically bound to the operator’s WebAuthn user ID at registration time. A credential generated for one operator is rejected if presented by another operator, because the relying party (Green-Got) validates the credential against the registered operator’s public key.


3. Registration and Approval Workflow

The back office implements a dual-control registration workflow that prevents unauthorized passkey activation:

3.1 Registration Steps

  1. The operator accesses the back office via the Tailscale VPN network. Tailscale’s WhoIs lookup resolves the operator’s identity to their email address, which must be registered in the internal Tailscale network.
  2. The operator initiates passkey registration. A WebAuthn challenge is generated with a 6-minute TTL and is consumed once used.
  3. The operator completes the WebAuthn ceremony on their mobile device (biometric/PIN verification occurs on-device).
  4. The passkey is created in Pending status — it does not grant login access.

3.2 Admin Approval (Dual Control)

  1. A team admin or super admin reviews the pending passkey and approves or rejects it.
  2. The following controls are enforced during moderation:
    • Self-approval is blocked — an operator cannot approve their own passkey
    • Self-rejection is blocked — an operator cannot reject their own passkey
    • Administration scope — team admins operate within their team scope; super admins operate across the entire company
    • Single active passkey — approving a passkey fails if the operator already has an active one
  3. Upon approval, the passkey status transitions to Active, and the operator receives a Slack notification.

3.3 Passkey Lifecycle States

┌──────────┐    Admin Approve    ┌──────────┐
│ Pending  │ ──────────────────► │  Active  │
└──────────┘                     └──────────┘
     │                                │
     │ Admin Reject                   │ Admin Delete
     ▼                                ▼
┌──────────┐                     ┌──────────┐
│ Rejected │                     │ Deleted  │
└──────────┘                     └──────────┘

[Screenshot placeholder: Admin panel showing the passkey approval interface with Pending/Active status]

3.4 Revocation and Deactivation Controls

The back office enforces immediate access revocation through three mechanisms:

Passkey Rejection When an admin or super admin rejects a pending passkey, the passkey transitions to Rejected status. A rejected passkey never becomes usable for authentication. The rejection reason is recorded in the audit trail (e.g., UnknownRequest, NewDevice, or a custom reason).

Passkey Deletion When an admin or super admin deletes an active passkey, the following occurs in a single database transaction:

  1. The passkey status transitions to Deleted
  2. All active sessions for that operator are immediately deleted from the bo_session table
  3. The status change is recorded in the audit trail

The operator is immediately logged out — any subsequent request with the previous session cookie is rejected. The operator cannot re-authenticate until a new passkey is registered and approved through the dual-control workflow.

Operator Archival When an operator is archived (deactivated), the following occurs in a single database transaction:

  1. The operator’s archived_at timestamp is set
  2. All of the operator’s passkeys (both Pending and Active) are cascaded to Deleted status, with the audit trail recording ArchiveOperatorCascade as the source
  3. All active sessions for that operator are immediately deleted

An archived operator has no valid passkey and no valid session. Since archival triggers offboarding (see below), the operator is removed from the Tailscale network entirely, making any access to the back office platform — including new passkey registration — impossible.

Operator Offboarding When an operator is archived from the back office, the offboarding process deletes the operator’s Google account. Since the Google account is the identity source for all internal tools — including the Tailscale network and Slack — this revocation cascades across the entire company infrastructure:

This ensures that archiving an operator from the back office results in a complete, company-wide access revocation.

Session Expiry as Defense in Depth In addition to immediate session deletion on revocation, all sessions expire automatically after 12 hours. This provides a secondary safeguard: even in the unlikely event of a race condition between revocation and session lookup, the session expires within the 12-hour window.

[Screenshot placeholder: Admin panel showing operator archival and passkey deletion actions]


4. Authentication (Login) Flow

Once a passkey is active, the login flow enforces that only the intended user gains access:

  1. The operator initiates login from a Tailscale-connected device.
  2. The server generates a WebAuthn authentication challenge scoped to the operator’s registered credential.
  3. The operator’s mobile device verifies the user (biometric or PIN) and signs the challenge.
  4. The server validates the signed response against the stored public key for that operator.
  5. A session is created with an HMAC-SHA256 signed token encoding the session_id and operator_id.

No password or shared secret is involved. The authentication factor (private key on the mobile device) never leaves the device.


5. Session Security Controls

ControlConfiguration
Token SigningHMAC-SHA256 with a server-side signing key
Token Contentsession_id + operator_id (protobuf-encoded, base64url)
Cookie FlagsHttpOnly, Secure, SameSite=Strict (production)
Session Expiry12 hours from creation
CSRF ProtectionPer-session CSRF token
LogoutDeletes the session record and clears the cookie

Sessions are bound to a single operator and are non-transferable. The HMAC signature prevents token tampering.


6. Audit Trail

Every passkey status change is recorded in the bo_passkey_status_history table:

FieldDescription
passkey_idThe passkey that was modified
modified_atTimestamp of the change
modified_by_kindOperator or System
modified_by_operator_idThe admin who performed the action (if operator-initiated)
modified_by_nameName of the actor
modified_to_statusThe new status (Active, Rejected, Deleted)
modified_to_reject_reasonReason for rejection (if applicable)

This audit trail provides traceability for every passkey lifecycle event, including who approved, rejected, or deleted a passkey and when.

[Screenshot placeholder: Passkey status history view in the admin panel showing approval/rejection events]


7. Sequence Diagrams

7.1 Passkey Registration and Approval

sequenceDiagram
    participant Operator
    participant Phone as Mobile Device
    participant BO as Back Office API
    participant Tailscale
    participant DB as Database
    participant Admin

    Operator->>BO: GET /auth/passkey/get_register_options
    BO->>Tailscale: WhoIs lookup (resolve email)
    Tailscale-->>BO: operator email (registered in Tailscale)
    BO->>DB: Create auth challenge (6-min TTL)
    BO-->>Operator: WebAuthn CreationChallengeResponse

    Operator->>Phone: Create credential (biometric/PIN)
    Phone-->>Operator: Signed credential

    Operator->>BO: POST /auth/passkey/verify_registration
    BO->>DB: Consume challenge (one-time use)
    BO->>DB: Find or create operator by email
    BO->>DB: Create passkey (status = Pending)
    Note over DB: Unique constraint: max 1 non-deleted passkey per operator
    BO-->>Operator: Passkey created (Pending)

    Admin->>BO: POST /operators/approve_passkey
    BO->>DB: Verify admin scope (team admin or super admin)
    BO->>DB: Verify actor ≠ target (no self-approval)
    BO->>DB: Set passkey status = Active
    BO->>DB: Record status history (who, when, what)
    BO-->>Admin: Operator details
    BO->>Operator: Slack notification (passkey approved)

7.2 Authentication (Login)

sequenceDiagram
    participant Operator
    participant Phone as Mobile Device
    participant BO as Back Office API
    participant DB as Database

    Operator->>BO: GET /auth/passkey/get_authentication_options
    BO->>DB: Find active passkey for operator
    BO->>DB: Create auth challenge (6-min TTL)
    BO-->>Operator: WebAuthn RequestChallengeResponse

    Operator->>Phone: Sign challenge (biometric/PIN)
    Phone-->>Operator: Signed assertion

    Operator->>BO: POST /auth/passkey/verify_authentication
    BO->>DB: Consume challenge (one-time use)
    BO->>DB: Validate credential against stored public key
    BO->>DB: Update passkey last_used_at
    BO->>DB: Create session (HMAC-signed token, 12h expiry)
    BO-->>Operator: Session cookie (HttpOnly, Secure, SameSite=Strict)

8. Summary of Controls Ensuring Unique Factor Assignment

ControlMechanism
One passkey per operatorDatabase unique partial index on operator_id
Cryptographic bindingWebAuthn credential is bound to operator’s user ID and relying party
User verificationBiometric or PIN required at each authentication
Non-exportable keyPrivate key never leaves the operator’s mobile device
Dual-control activationAdmin approval required; self-approval blocked
Scoped administrationTeam admins operate within their team; super admins operate company-wide
Immediate revocationPasskey deletion and operator archival immediately invalidate all sessions
Offboarding cascadeOperator archival triggers Google account deletion, revoking Tailscale and all SSO access
Audit trailEvery status change is logged with actor identity and timestamp
Session isolationHMAC-signed, per-operator, time-limited sessions
Network-level identityOnly email addresses registered in the internal Tailscale network are granted access

Unique customer environment authentication

Provide evidence that in scenarios where an employee can access multiple customer environments (physical or logical) they have a unique ID for each environment accessed.

Evidence

This requirement is not applicable to Green-Got.

PCI DSS Requirement 8.2.3 is an additional requirement for service providers only. It requires that employees with remote access to multiple customer premises use unique authentication factors for each customer environment. Green-Got operates as a banking institution providing financial services directly to consumers and business customers. It does not provide banking-as-a-service or payment infrastructure to other financial institutions or service providers and does not operate separate customer-managed PCI environments.

Green-Got’s employees access a single Green-Got-owned PCI DSS environment. No customer-specific operator identities or per-customer authentication factors are required under this control.

Unix/Linux access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login attempts on 3 (or all if fewer than 3) different unix/linux system types using the vendor-supplied default accounts and passwords. The evidence must specify the default accounts/passwords used.

Evidence

We use one Linux distribution in our CDE: Amazon Linux 2023 running on AWS EC2 instances.

Amazon Linux 2023 does not ship with vendor-supplied default passwords:

Additionally, our infrastructure makes SSH access architecturally impossible. The Pulumi infrastructure-as-code configuration (src/infrastructure/environment/setup_region.ts) enforces:

  1. No SSH key pairs — The EC2 Launch Template does not specify a keyName. No SSH key pairs are created or managed in the infrastructure.
  2. Port 22 is not open — No security group ingress rule allows traffic on port 22. The instance security group only permits traffic from within VPC CIDR blocks.
  3. No SSH daemon reliance — Shell access to instances is provided exclusively through AWS Systems Manager Session Manager, which authenticates via IAM and logs all sessions to CloudTrail.

References:

Since no default passwords exist and SSH access is not configured, there are no vendor-supplied credentials to test against.

Untrusted keys and certificates rejected ❌

Provide vendor documentation, configuration information or an example failure message showing that untrusted certificates and keys are denied by default when they are used to attempt access to the environment.

User account non-repudiation ⏱️

Screenshot or other evidence showing that users are given unique account identifiers that can be associated with individual employees

Evidence

@chloe

Unique User Identification

Every back office user is represented as a bo_employee with:

No two employees share the same identifier or email. The uniqueness constraint is enforced by PostgreSQL, making it impossible to create duplicate accounts.

Database schema (src/db/migrations/20240425145854_bo_user.up.sql):

create table
  bo_employee (
    id int primary key generated always as identity,
    email text not null unique,
    ...
  );

Domain model (src/bo/src/domain/bo_operator.rs):

pub struct BoOperator {
    pub id: i32,
    pub email: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub archived_at: Option<DateTime<Utc>>,
    pub last_sign_in_at: Option<DateTime<Utc>>,
}

Session Binding to Individual User

Each session is cryptographically bound to a specific employee via HMAC-SHA256 signed tokens. The session token encodes both session_id and operator_id and is verified on every request.

Session token structure (src/bo/src/infrastructure/adapters/session_token.rs):

struct BoSessionTokenProto {
    session_id: i32,
    operator_id: i32,
}

The token is signed with HMAC-SHA256 using a secret key. On every request, the middleware:

  1. Extracts the session cookie
  2. Verifies the HMAC-SHA256 signature
  3. Queries the database to confirm the session exists and matches the operator
  4. Injects BoSessionData { session_id, operator_id } into request extensions

Authentication middleware (src/services/back_office_api/src/auth/auth_middlewares.rs):

pub async fn auth_middlewares(...) -> Result<Response, StatusCode> {
    let session_cookie = extract_session_cookie(&cookies);
    match session_cookie {
        Some(session_token) => {
            let payload = BO_SESSION_TOKEN_SERVICE.verify(&session_token)...;
            match read_session(&pool, payload.session_id, payload.operator_id).await {
                Ok(session_data) => {
                    extensions.insert(session_data);
                    ...
                }
            }
        }
        None => Err(StatusCode::UNAUTHORIZED),
    }
}

Every route handler receives the authenticated operator identity through BoSessionExtractor, making it impossible to process requests without a verified operator_id.

Audit Trail Linked to Individual Users

All back office actions are recorded in the bo_audit_entry table with a mandatory foreign key to the employee who performed them:

create table
  bo_audit_entry (
    id int primary key generated always as identity,
    created_at timestamp not null default current_timestamp,
    employee_id int not null references bo_employee (id),
    action text not null,
    entry_type BoAuditEntryType not null,
    context jsonb,
    success boolean not null,
    errors text[]
  );

Each audit entry records who (employee_id), what (action, entry_type), when (created_at), context (JSON payload), and outcome (success, errors).

Dual-Control Validation Requests

Sensitive operations require a two-person approval workflow tracked in the bo_validation_request table:

create table
  bo_validation_request (
    id int primary key generated always as identity,
    employee_id int not null references bo_employee (id),
    validator_id int references bo_employee (id),
    action text not null,
    context jsonb,
    status BoValidationRequestStatus not null,
    ...
  );

Both the requestor (employee_id) and the approver (validator_id) are individually identified and recorded.

Role-Based Access Control

Employees are assigned to teams with specific permissions. Team membership and admin privileges are tracked per-employee:

create table
  bo_employee_team (
    employee_id int not null references bo_employee (id),
    team_id int not null references bo_team (id),
    admin boolean not null default false,
    primary key (employee_id, team_id)
  );

Additional Session Tracking

Each session records the user_agent string, providing device-level attribution alongside the user identity. The bo_employee table also tracks last_sign_in_at, preserving a history of authentication events per user.

User accounts are unique ❌

Provide screenshots or other evidence that all users have unique user accounts. Guidance: This is most often uploaded as a screenshot of IAM roles for regular users, a screenshot of user accounts within those roles, and then a cross-reference of employee names with users accounts to identify any shared accounts.

User identity is verified prior to resetting password ❌

@chloe Evidence that non-face-to-face password resets require user identity to be verified. Guidance: Provide screenshot or evidence of the configuration and and related process used to verify identity (text/email verification, self-service portal)

Evidence

VPC Flow Logs enabled ✅

This test checks whether your AWS Virtual Private Clouds (VPCs) have VPC Flow Logs enabled for network traffic monitoring.

Evidence

We are deactivating this Vanta test because we use AWS GuardDuty Runtime Monitoring for these workloads instead of VPC Flow Logs.

AWS documents that Runtime Monitoring uses a GuardDuty security agent on the EC2 instance and sends runtime events from the machine to GuardDuty. This gives us direct host-level visibility for monitoring the workload instead of relying on VPC-level network metadata alone:

VPC Flow Logs capture IP traffic metadata for network interfaces in a VPC. Because we run the GuardDuty agent directly on the machine, we rely on GuardDuty Runtime Monitoring as the replacement control for workload monitoring and alerting rather than duplicating that coverage with VPC Flow Logs.

Vendor Due Diligence

Provide documentation showing that your organization performs formal due diligence prior to engaging third-party vendors. This evidence should demonstrate that vendors are evaluated for their ability to meet your security, privacy, and compliance requirements—particularly where cardholder data or critical services are involved.

Evidence

Green-Got performs formal due diligence on all third-party vendors that interact with or have the potential to impact the Cardholder Data Environment (CDE) prior to final contract agreement. As part of the procurement process, vendors are evaluated for their ability to meet security, privacy, and compliance requirements before any contractual agreement is finalized.

Vendor due diligence is documented and tracked in Vanta: Vendors

Vendor Risk Assessments

Provide completed risk assessments for third-party vendors that detail the level of risk posed based on the sensitivity of data handled, services provided, and the vendor’s security posture. Risk assessments help your organization determine the level of oversight required for each vendor and support ongoing vendor risk management efforts.

Evidence

Vendor Risk management is performed in Vanta here: Vendors

Vendor credentials used securely

Screenshot, vendor documentation or other evidence that passwords used by 3rd party vendors or for access to externally hosted applications are stored and transmitted securely using industry accepted methods such as salting and hashing, secure encryption, etc. Note: For more information see https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines

Evidence

Green-Got avoids vendor-local passwords for third-party SaaS access wherever the vendor supports federated authentication. Access to externally hosted applications is primarily tied to Google account login through OAuth 2.0 / OpenID Connect, so users authenticate with Google instead of maintaining a separate password in each vendor application.

AWS IAM Identity Center is one of the limited vendor services where password authentication required to be used. In that case, password handling is performed by AWS IAM Identity Center.

For other vendors where Google OAuth is not available, the supporting screenshots and vendor documentation are maintained in Vanta and attached to the vendor records used for PCI evidence collection. This includes Render and any other in-scope externally hosted applications that rely on vendor-managed authentication.

This evidence aligns with the referenced NIST cryptographic guidance: Cryptographic Standards and Guidelines

Vendor-supplied defaults are changed on all systems

Upload vendor documentation showing the default settings for the relevant technology (e.g., default usernames, passwords, SNMP strings, encryption keys, or services). Then provide evidence demonstrating that these defaults have been removed, disabled, changed, or secured. This may include a ticket or change record confirming that default settings were reviewed and remediated, or technical scan results or configuration exports showing that no default accounts or settings remain active. Note: Vendor documentation is required to validate what the original default values were. The evidence must clearly show comparison and remediation.

Evidence

Green-Got does not use any vendor, service provider, or externally hosted application in the cardholder data environment (CDE) that relies on vendor-supplied default credentials.

In-scope vendor and service access uses one of the following authentication patterns:

No in-scope vendor account, service account, administrative console, database, network service, or externally hosted application is operated with a vendor default username, password, SNMP string, encryption key, or shared default account. Initial administrative identities for vendor products are configured by Green-Got during provisioning before the service is used in the CDE.

This means there are no vendor default credentials to disable.

Vulnerabilities remediated

ASV

Provide evidence of remediation of discovered vulnerabilities from an ASV Scan. This should include the severity of the finding, how it was resolved, and evidence that formal change management processes were followed for its remediation.

Evidence

There are no vulnerabilities reported yet so we cannot show any treatment history.

ASV Scan Screenshot

Medium and lower

Provide documentation showing that medium and lower risk vulnerabilities identified through vulnerability scans or assessments are tracked, assessed, and remediated within a timeframe defined by the TRA. This evidence supports the organization’s broader vulnerability management program and demonstrates attention to reducing the overall security risk, even for non-critical issues. Note: Only provide evidence here if you’re NOT using one of Vanta’s vulnerability scanner integrations.

Evidence

We are using Vulnerability scanners (GitHub Dependabot, Amazon Inspector) integrated into Vanta. You can find our history here Vanta Vulnerabilities

Sample - External Pentest

Provide evidence of remediation of discovered vulnerabilities from an external penetration test. This should include the severity of the finding, how it was resolved, and evidence that formal change management processes were followed for its remediation.

Evidence

There are no vulnerabilities reported yet so we cannot show any treatment history.

Sample - Internal Pentest

Provide evidence of remediation of discovered vulnerabilities from an internal penetration test. This should include the severity of the finding, how it was resolved, and evidence that formal change management processes were followed for its remediation.

Evidence

There are no vulnerabilities reported yet so we cannot show any treatment history.

Sample of remediated vulnerabilities

Provide evidence of remediation via tickets of high-severity findings from your most recent periodic vulnerability scan. Note: Only provide evidence here if you’re NOT using one of Vanta’s vulnerability scanner integrations.

Evidence

We are using Vulnerability scanners (GitHub Dependabot, Amazon Inspector) integrated into Vanta. You can find our history here Vanta Vulnerabilities

Vulnerability Intelligence Sources

Provide documentation showing active monitoring of trusted vulnerability intelligence sources. Evidence may include screenshots or exports of subscriptions to feeds such as US-CERT/CISA alerts, vendor mailing lists (e.g., Microsoft, Oracle, Apache), CVE/NVD feeds, or integrations with vulnerability tools such as Rapid7, Qualys, Tenable, Snyk, or GitHub Dependabot.

Evidence

Green-Got uses GitHub Dependabot for continuous monitoring of known dependency vulnerabilities. Dependabot alerts are tracked in GitHub and remediation pull requests are created through the daily security alert automation.

Vulnerability Management Procedure or SOP

Provide documentation outlining your organization’s formal vulnerability management procedure. The procedure should clearly describe how new vulnerabilities are tracked, triaged, and risk-ranked, the frequency with which they are reviewed, and the roles responsible. The documentation must also demonstrate that the process applies to custom-developed applications, third-party software, and operating systems.

Evidence

Green-Got maintains a formal vulnerability management process for bespoke application code, third-party software and dependencies, and operating systems used in the cardholder data environment.

1. Sources used to detect new vulnerabilities

New vulnerabilities are identified from multiple sources that run continuously or on a defined schedule:

2. How vulnerabilities are tracked

Green-Got tracks vulnerabilities in the system where they are discovered and uses automation to keep them moving toward remediation:

This gives Green-Got a continuous tracking path for third-party component issues and scanner findings associated with deployed systems.

3. Triage and risk ranking

Each newly identified vulnerability is triaged based on:

Risk ranking follows the severity and exposure information from the authoritative source of the finding:

When the issue is dependency-based, the preferred remediation is to update or replace the vulnerable package through an automated pull request. When the issue affects custom-developed code, infrastructure configuration, or the operating environment, the responsible engineering owner implements the required code or infrastructure change through the standard change-management and deployment process.

4. Review frequency

Vulnerability review occurs at several layers:

5. Roles responsible

The vulnerability management process uses the following roles already defined in Green-Got policies:

RoleResponsibility in the vulnerability management process
Chief Technology Officer (CTO)Owns oversight of security tooling, production security processes, and remediation execution for infrastructure and software under Green-Got control.
Head of RiskOversees compliance with PCI DSS and validates that the vulnerability management process remains aligned with the broader risk and compliance program.
Cybersecurity EngineerSupports risk assessment activities, documents relevant security risks, and escalates material exposure to leadership.
Developer/Systems OwnerImplements remediations in code, infrastructure, and configuration for the systems they own and ensures fixes move through review, testing, and deployment.

These responsibilities are consistent with Information Security Roles and Responsibilities Policy, Secure Development Policy, and Operations Security Policy.

6. Scope of the process

This procedure applies to all major vulnerability classes in PCI DSS scope:

7. Resulting operating model

In practice, Green-Got’s vulnerability management procedure is:

  1. Detect new vulnerabilities from GitHub, Vanta-integrated scanners, pull-request scanning, and Amazon Inspector.
  2. Track each finding in the originating system until closure, with Vanta serving as the consolidated evidence trail for integrated scanners.
  3. Triage the finding by severity, PCI scope, exploitability, and affected layer.
  4. Remediate dependency issues through automated pull requests whenever possible, and remediate custom-code, configuration, and operating-system issues through the standard engineering change process.
  5. Verify closure by the absence of the alert in GitHub/Vanta or by a clean follow-up scan.

This procedure aligns with the related PCI DSS evidence already maintained in this repository for vulnerability intelligence, supported systems, vulnerable libraries, and critical security patching.

Vulnerability scan

Provide a screenshot of findings from your most recent periodic vulnerability scans to identify vulnerable assets. Note: Only provide evidence here if you’re NOT using one of Vanta’s vulnerability scanner integrations.

Evidence

We are using Vulnerability scanners (GitHub Dependabot, Amazon Inspector) integrated into Vanta. You can find our history here Vanta Vulnerabilities

Web application firewall is enabled to monitor and protect traffic on public facing systems

Provide evidence that a web application firewall (WAF) is enabled and configured to actively block or send alerts in response to common web attack vectors. Guidance: Provide evidence of WAF settings, including:

Alternately: If WAF is not used, upload evidence (such as technical testing report results) showing that an application security assessment was performed on web-facing applications impacting the CDE (such as payment and application administration pages).

Evidence

We use AWS WAF. The configuration is kept here as infrastructure as code

Windows access defaults changed

Vendor-supplied default accounts and passwords Provide screenshots showing failed login attempts on 3 different windows system types using the vendor-supplied default accounts and passwords. The evidence must specify the default accounts/passwords used.

Evidence

Green-Got does not use any Windows-based systems within the Cardholder Data Environment (CDE) or for administrative access to production systems. All administrative access is performed from non-Windows systems. Therefore, there are no Windows default accounts or passwords requiring validation, and this control is not applicable.

Wireless access points are configured securely

Evidence that wireless access points are configured securely and use only strong encryption for transmission. Guidance: Upload configuration showing enabled encryption protocols, ensuring that insecure protocols such as WEP are disabled and devices are configured securely. If no wireless access points are present in the CDE, use the ‘Deactivate’ button and include an explanation.

Evidence

We don’t have any wireless access points.

Wireless network connections encrypted

Provide evidence that wireless network connections are encrypted and secured in accordance with industry standards. Guidance: Industry standards include the use of WPA3, WPA Enterprise, and TLS 1.2+, as applicable.

Evidence

We don’t have any wireless networks.

Wireless network infrastructure inventory

Provide an inventory of all wireless network devices that are in scope that includes device model, type, serial number, etc, and is reviewed and approved annually.

Evidence

Green-Got does not operate or manage any corporate wireless network infrastructure or Wi-Fi access points. The organization is fully cloud-hosted on AWS and operates in a remote-first model without company-managed office networks.

As no wireless access points are deployed or managed by Green-Got, there are no devices to include in a wireless infrastructure inventory.

Therefore, this control is not applicable.

Contributing

Crates and modules structure

For business related code, we follow Domain-Driven Design (DDD) principles. Our backend documentation provides you with an overview of DDD and how we structure our codebase. The main advantage we see with this approach is that it helps us to ensure that business logic is correctly covered by enforcing invariants at the domain level.

Prefer colocating implementations in the crate that owns the behavior instead of spreading them across crates that are meant to expose APIs or traits only. Keeping implementations in their owning crate reduces the risk of cyclic dependencies when API-only crates start depending on concrete implementations (or vice versa), which makes the crate graph harder to evolve.

Technical and utility crates that don’t have a predominant business logic don’t need to follow this approach and can use a more flat structure that allows good code colocation. If you have to start such crate you can take a look at some of our existing ones:

Prefer DateTime<Utc> over NaiveDateTime and timestamptz over timestamp

timestamptz over timestamp is simply a consequence from DateTime<Utc> over NaiveDateTime but it also recommended by the Posgres Wiki

Database

Never use varchar or varchar(n)

varchar is the equivalent of text. So for consistency use text. Varchar is slower and uses more storage than text. Postgres Docs

Always use create index concurrently over create index

Use create index concurrently because PostgreSQL will build the index without taking any locks that prevent concurrent inserts, updates, or deletes on the table. A standard index build locks out writes (but not reads) on the table until it’s done.

Currently because of Github issue that means that create index concurrently need to be in migration files containing only a single statement

How to use PgExecutor and PgTransaction

Every function that interacts with the database should accept either a PgExecutor or a PgTransaction as part of the arguments. Use a PgExecutor if your function only does a single call to the database and use a PgTransaction if it does multiple. We want our calls to be composable so only the top level function like a API handler starts and commits a transaction. You can find more details about lock mechanims and concurrency in our database concurrency documentation.

Use i32 or i64 for internal database tables

Sequential ids are performant, index nicely and are easy to use. When the id is not part of our public API interface use i32 or i64.

Use macros::time_sortable_id for database tables where you want to use the id publicly

To make our id’s not guessable like with i32. Also the prefix helps you quickly identify the table the id belongs to when shared to you in a support ticket.

Database structs naming convention

Structs that simply represent tables should be called {}Record. These structs should have a non record counterpart and implement TryFrom or From to convert between {}Record and {}. Record structs should stay local to the store functions only their counterpart should be public.

The idea behind this is that Rust’s type system is a lot more expressive than what you can represent in SQL. The Rust version of your type should try to represent your data model as closely as possible while the SQL version is simply there to fit the data model into our database.

IDs created from macros::time_sortable_id should be part of the public type the record struct should represent it as String and your TryFrom implementation should call Id::try_from for the conversion

Use Postgres CTE’s

If you execute 2 queries in a single file consider using a CTE to combine them into a single operation. This likely gets rid of your need for a database transaction and reduces the execution time of the overall function because there are less roundtrips with the database.

Errors

Reference to type that implements Copy

Assertions

Use of Default

Default can be used to easily initialize data for unit tests. However, it should always be kept in mind that it should also be a safe default to use outside of tests for real data. For a test specific use, prefer using the Dummy trait from the fake crate.

Avoid hidden cloning behind references

When writing functions or traits, avoid taking &T and then cloning most of its fields internally. This pattern has two problems:

  1. It forces unnecessary clones — The caller always pays the cost of cloning, even when they could have given up ownership of T.
  2. It removes caller flexibility — The caller loses the option to transfer ownership, which would avoid cloning entirely.

Instead, take T by value. This lets the caller choose:

❌ Avoid: Taking a reference and cloning internally

impl From<&RejectionEntry> for EntryRecord {
    fn from(value: &RejectionEntry) -> Self {
        Self {
            // to_string() and to_owned() are clones in disguise!
            id: value.id.to_string(),
            transaction_id: value.transaction_id.to_owned(),
            amount: value.amount.value as i64,
            source_bank_account_id: value.source_bank_account_id.to_string(),
            destination_bank_account_id: value.destination_bank_account_id.to_string(),
        }
    }
}

// Caller is forced to clone even if they don't need `entry` anymore
let record = EntryRecord::from(&entry);

✅ Prefer: Taking ownership and letting the caller decide

impl From<RejectionEntry> for EntryRecord {
    fn from(value: RejectionEntry) -> Self {
        Self {
            id: *value.id,
            transaction_id: *value.transaction_id,
            amount: value.amount.value as i64,
            source_bank_account_id: *value.source_bank_account_id,
            destination_bank_account_id: *value.destination_bank_account_id
        }
    }
}

// Caller gives up ownership — no clone needed
let record = EntryRecord::from(entry);

// Or caller explicitly clones when they need to keep the value
let record = EntryRecord::from(entry.clone());

String formatting

The Display trait should represent the technical format used in APIs, storage, and system operations. For human-readable or special formats, use dedicated methods (e.g., to_human_readable()). This avoid errors where a technical format is expected but a human-readable format is provided instead.

Example:

impl fmt::Display for DummyStruct {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // For example, for our technical format, only the first element matters
        write!(f, "{}", self.0.to_string())
    }
}

impl DummyStruct {
    pub fn to_human_readable(&self) -> String {
        // Could be any format suitable for humans, could be spaces, dashes, ...
        format!("field1: {}; field2: {}", self.0.to_string(), self.1.to_string())
    }
}

For sensitive data (PANs, secrets, …), use the Sensitive derive and wrapper types, which provide masked Display/Debug implementations (rendering "***") for use in logs and formatted output. See utils::Sensitive and macros::Sensitive.

Core Banking

1. Cardholder Environment

Cardholder Data Environment (CDE) Overview

This document explains how the Cardholder Data Environment (CDE) is structured when Green-Got acts as a card issuer. It defines what data must be protected under PCI DSS, where it resides, and how partners interact with our systems. For cryptographic implementation details, see the companion Cryptography & Key Management document.

1. Ecosystem actors

We interact with multiple partners across two transaction domains: physical card transactions (chip, contactless, ATM) and online card transactions (e-commerce, virtual cards):

PartnerRole in the flowPCI relevance
ExceetCard manufacturing, personalization payload intake.Receives PAN-derived data and card profiles.
MastercardPayment network for transaction routing.Defines PAN ranges, PIN verification standards, issuer compliance.
CB (Carte Bancaire)French domestic payment network.Network-specific transaction routing and settlement.
ArkéaPrincipal member, settlement, sponsor banking, SEPA and instant payment processing.Provides EMV chip key material; handles card transaction clearing and SEPA/instant payment rails.
Apata3D Secure authentication provider for online transactions.Validates e-commerce authentication (AAV).
AWS KMSManaged HSM service for the key-encrypting key and data key wrapping.Protects KMS-generated data keys used by CBS for application-side ChaCha20-Poly1305 encryption at rest.
AWS Payment CryptographyManaged payment HSM service for cryptographic operations and transport protection.Used for online PIN verification, PIN reveal and set flows, EMV PIN change script generation, and transport-specific cryptographic operations such as Mastercard PIN transport and Exceet personalization.

Throughout this doc, PAN stands for Primary Account Number, PIN for Personal Identification Number, CDE for Cardholder Data Environment, CHD/SAD for Cardholder Data and Sensitive Authentication Data respectively, CBS for Core Banking Service (Green-Got’s isolated module for core banking operations including card issuing, transaction authorization, and cardholder data management), and AAV for Account Authentication Value (3DS 2.x authentication cryptogram).

2. Data classes inside the CDE

PCI DSS distinguishes between Cardholder Data (CHD) and Sensitive Authentication Data (SAD):

2.1 Cardholder Data (CHD)

Required: PAN (Primary Account Number)
Optional: Cardholder name, expiration date

CHD may be stored if encrypted and access-controlled. The PAN must always be rendered unreadable when at rest (encryption, tokenization, or truncation/masking).

PCI DSS Scope Note: Cardholder name and expiration date are classified as CHD only when stored with (or linked to) the PAN. Green-Got uses a PAN pool architecture where:

2.2 Sensitive Authentication Data (SAD)

Includes: Card verification codes (CVC/CVV/CVC2/CVV2), PINs and PIN blocks.

PCI DSS Note for Issuers: PCI DSS Requirement 3.2.3 prohibits CVC storage for merchants and service providers after authorization. However, as the card issuer, Green-Got is permitted to store CVCs encrypted for legitimate issuer operations (transaction validation, card manufacturing, card replacement). We generate CVCs and are the authoritative source.

Green-Got’s SAD storage:

Note: Green-Got supports magnetic stripe transactions for international compatibility (e.g., US gas stations). Track data is ephemeral only (never stored, processed in-memory during transaction only).

2.3 CDE boundary definition

Only systems that store, process, or transmit clear CHD or SAD belong to the CDE. Our architecture minimizes CDE scope by:

  1. HSM-backed key management with application-side encryption: Green-Got uses HSMs for key lifecycle operations, with actual data encryption performed in-application:

    • AWS KMS: Protects the AES-256 KEK and generates KMS-encrypted data key blobs (HSM-backed). CBS performs application-side ChaCha20-Poly1305 encryption of cardholder data at rest using the plaintext data keys.
    • AWS Payment Cryptography: HSM-based transport and payment cryptographic operations, including Visa PVV verification, server-side PIN set and reveal flows, EMV PIN change script generation, and PEK-based manufacturer communication.
    • Arkéa generates all payment network cryptographic keys and distributes them to Green-Got and partners.
  2. Encryption at rest: All persisted CHD/SAD uses AWS KMS-backed envelope encryption. CBS encrypts payloads with ChaCha20-Poly1305 using KMS-generated data keys, and encrypted data is prefixed with the KMS-encrypted data key needed for decryption. Only encrypted ciphertext or tokenized references exist in databases.

  3. Partner communication and PIN translation: When exchanging data with partners or handling PIN flows, issuer-side ciphertext is decrypted from storage with KMS when needed, then AWS Payment Cryptography performs the required transport protection or verification step (CBS-controlled reveal protection, PEK for Mastercard transport, PEK for Exceet), then the clear data is discarded.

  4. Tokenizing for external APIs: Public identifiers (PAN tokens, UUIDs) replace real PANs in API responses and logs.

  5. In-memory only for display: Clear CHD exists in application memory only when absolutely necessary (e.g., deriving masked PAN for UI display, re-encrypting for partner communication), then immediately discarded.

This design keeps most application services outside the CDE scope.

For the complete architecture diagram showing HSM layers, encryption patterns, and data flows, see Cryptography & Key Management - Section 2.3.

3. Data handling rules

This table defines how each data element is stored, transmitted, and processed:

Data elementClassificationStorage ruleTransit ruleProcessing location
PAN (full)CHDEnvelope-encrypted with ChaCha20-Poly1305 using KMS-generated data keys; never stored in clear.TLS 1.2+; clear only in CBS memory for partner communication.CBS (in-memory), AWS KMS (data key wrapping/unwrapping), AWS Payment Cryptography (for TDES re-encryption to Exceet).
PAN (masked)Non-sensitiveNot stored; derived on-the-fly from encrypted full PAN.TLS 1.2+.CBS generates when needed for display.
PAN tokensNon-sensitiveUUID/TSID stored in clear.TLS 1.2+.CBS and downstream services.
PINSADNot stored by default; exists only as cardholder-entered data in the dedicated app iframe, ATM, or terminal flows.TLS 1.2+ from the dedicated app iframe to CBS for app set/change and reveal display; PEK-protected in payment network flows.Verified or transformed in AWS Payment Cryptography; not persisted in issuer storage.
PVVSADEncrypted with ChaCha20-Poly1305 using KMS envelope encryption; stored as issuer-side reference data for online PIN verification.TLS 1.2+ when accessed by CBS; never sent to terminals or partners.Generated by AWS Payment Cryptography GeneratePinData; used by VerifyPinData.
Issuer-side encrypted PIN blockSADPersisted by the issuer: retained under PEK_MC_AUTH by AWS Payment Cryptography, then encrypted at rest with ChaCha20-Poly1305 using KMS envelope encryption.TLS 1.2+ when accessed by CBS; translated inside AWS Payment Cryptography before app or partner use.Used for reveal PIN, Exceet transmission, and EMV PIN change preparation.
Transport PIN blockSADEphemeral only when generated for Mastercard online PIN transport or manufacturer communication.TLS 1.2+ CBS↔AWS Payment Cryptography and network rails as applicable.Formatted according to the receiving boundary: Mastercard-required format under PEK_MC_AUTH, Exceet ISO-0 under PEK_EXCEET; discarded unless retained as the issuer-side encrypted PIN block described above.
CVC/CVVSADEncrypted with ChaCha20-Poly1305 using a KMS-generated 256-bit data key; stored for issuer operations (validation, card manufacturing).TLS 1.2+; clear only in CBS memory for manufacturing communication.Generated by Green-Got during card issuance, encrypted and stored for card lifecycle management.
Track data (magnetic stripe)SADNever stored (ephemeral only).Transits through Mastercard/CB rails to CBS; in-memory only during transaction.CBS processes during transaction authorization (magstripe supported for international compatibility), then discarded.
ARQC/ARPC (cryptograms)SADNot stored; validated during transaction only.TLS 1.2+ network↔CBS.Validated by CBS using keys from Arkéa.
AAV (Account Authentication Value)SADNot stored; validated during authorization only.TLS 1.2+ Apata↔CBS.Validated by CBS using shared HMAC-256 key from Arkéa.
Cardholder name, expiryCHDEncrypted at rest (CHD when stored with PAN reference).TLS 1.2+.CBS and reporting pipelines.
BIN (Bank Identification Number)Non-sensitivePlaintext.TLS 1.2+.Multiple services.

3.1 Processing principles

4. PCI DSS compliance boundaries

4.1 In-scope systems (CDE)

4.2 Out-of-scope systems (non-CDE)

4.3 Controls

2. Cryptography & Key Management

Cryptography & Key Management for the Card Domain

This document explains how Green-Got manages cryptographic keys and encrypts cardholder data in compliance with PCI DSS. It covers the key architecture, encryption patterns, HSM usage, and partner communication flows. For CDE structure and data classification, see Cardholder Environment.

Document Control

FieldValue
Document StatusSubmitted
Effective Date2026-02-10
Last Updated2026-06-04
Document OwnerCore Banking Team
Review FrequencyAnnual or after a material cryptographic architecture change

1. Guiding principles

  1. Arkéa as key authority — All cryptographic keys for payment network operations are generated by Arkéa and distributed to Green-Got and partners. Green-Got does not generate keys for partner communication.

  2. Storage always uses ChaCha20-Poly1305 — All internal storage (PostgreSQL) uses AWS KMS envelope encryption: AES-256 KEK + data keys (from KMS), with ChaCha20-Poly1305 for the actual data encryption. This includes PAN, PVV, the issuer-side encrypted PIN block, CVC, cardholder metadata, and related CHD/SAD. Arkea payment keys (KCVX, PVK, PEK, IMK) are never used for storage - they are only for payment cryptographic operations and transport protection.

  3. Payment HSM operations are transport-specific — AWS Payment Cryptography is used for online PIN verification, server-side PIN set/change and reveal flows handled through CBS and the dedicated PIN iframe, EMV issuer scripts, and partner communication. Dedicated keys are kept per trust boundary, including a PEK for Mastercard PIN transport and a separate PEK for Exceet manufacturing.

  4. Decrypt-then-re-encrypt pattern — When sending data to partners: (1) decrypt from PostgreSQL through the KMS-backed envelope encryption flow (ChaCha20-Poly1305 using a KMS-generated 256-bit data key), (2) transform to clear in CBS memory, (3) encrypt with partner’s key via AWS Payment Cryptography (TDES or AES), (4) transmit, (5) zeroize clear data from memory.

  5. Data key prefixing — All encrypted data stored with the encrypted data key (length-prefixed), enabling decryption without requiring key metadata lookup. AWS KMS handles KEK rotation transparently.

  6. Minimum exposure — Clear CHD/SAD exists only transiently in CBS memory during partner communication or display operations, then immediately zeroized.

  7. Auditable flows — Every key import, encryption, and decryption operation produces audit evidence (CloudTrail, application logs with redaction).

Acronyms: PAN (Primary Account Number), PIN (Personal Identification Number), CVC (Card Verification Code), ZMK (Zone Master Key), KEK (Key Encryption Key), TDES (Triple Data Encryption Standard), CHD (Cardholder Data), SAD (Sensitive Authentication Data), CBS (Core Banking Service), AAV (Account Authentication Value), ARQC/ARPC (Application Request/Response Cryptogram).

2. Cryptographic architecture overview

Green-Got’s encryption architecture uses two distinct HSM services for different purposes:

2.1 AWS KMS (ChaCha20-Poly1305 with AES-256 KEK - Internal Storage Only)

Purpose: All data-at-rest encryption for cardholder data in PostgreSQL. Internal to Green-Got only - not for partner communication.

Architecture: HSM-backed key management with application-side encryption:

Algorithms:

Key structure:

Use cases:

Key source: KEK created in AWS KMS. PAN lookup HMAC keys are generated by Green-Got and stored as protected AWS Systems Manager Parameter Store secrets. These are Green-Got internal keys, not provided by Arkéa.

PAN lookup HMAC key lifecycle:

Critical: AWS KMS data keys cannot be used for partner communication. Partners have no way to decrypt data encrypted with Green-Got’s KEK. For partner communication, use ZMK keys shared from Arkéa (see section 2.2).

2.2 AWS Payment Cryptography (Transport and Payment Cryptographic Operations)

Purpose: payment cryptographic operations and transport protection using shared payment keys.

Algorithms:

Key structure: ZMK (Zone Master Key) provided by Arkéa for wrapping/unwrapping partner keys. ZMK can be either TDES or AES depending on the partner’s requirements.

Use cases:

Key source: All keys provided by Arkéa and imported into HSMs.

Critical distinctions:

2.3 Architecture diagram

graph TB
    subgraph Arkéa["Arkéa (Key Authority)"]
        KeyGen["Generates all payment network keys
ZMK, PVK, PEK_MC_AUTH, PEK_EXCEET, KCVX, IMK, KAAV"] end subgraph Partners["External Partners"] Exceet["Exceet
(Card Manufacturing)
TDES"] Mastercard["Mastercard
(Payment Network)
TDES/AES"] Apata["Apata
(3DS Provider)
HMAC-256"] end subgraph GreenGot["Green-Got Core Banking Service (CBS)"] subgraph HSMs["HSM Layer"] KMS["AWS KMS
(AES-256 KEK + data keys)
ChaCha20-Poly1305 for data
Internal storage only"] PayCrypto["AWS Payment Cryptography
(TDES + AES + ECDH)
Arkea-provided payment keys
Payment crypto and transport protection"] end subgraph Processing["Processing Layer"] Flow["Two-stage encryption pattern:
1. Decrypt from storage (KMS)
2. Re-encrypt for partner (Payment Crypto)
3. Transmit
4. Zeroize clear data"] end subgraph Database["PostgreSQL Database"] DBData["• PAN (encrypted with KMS)
• PVV (encrypted with KMS)
• Issuer PIN block (encrypted with KMS)
• CVC (encrypted with KMS)
• Cardholder metadata (encrypted with KMS)
• Security state and counters"] end end Arkéa -->|"Distributes keys"| GreenGot Arkéa -->|"Distributes keys"| Partners KMS -->|"Key generation/wrapping"| Processing Processing -->|"Store encrypted data"| Database PayCrypto -.->|"HSM-based re-encryption"| Processing GreenGot -->|"Manufacturer PIN block +
card personalization"| Exceet GreenGot <-->|"Online PIN transport +
transaction authorization"| Mastercard Apata -.->|"AAV validation
HMAC-256"| GreenGot style Arkéa fill:#e1f5ff style Partners fill:#ffe1f5 style HSMs fill:#f0fff0 style Processing fill:#fff5e1 style Database fill:#f5f5f5 style KMS fill:#90EE90 style PayCrypto fill:#FFD700

Key points:

3. Key management

3.1 Key sources and ownership

Key TypeGenerated ByDistributed ToPurposeAlgorithm
KEK (KMS)AWS KMSGreen-Got onlyMaster key for wrapping data keysAES-256
Data KeysAWS KMS (wrapped by KEK)Never exportedEncrypt individual data elements with ChaCha20-Poly1305256-bit symmetric data keys
ZMK (Zone Master Key)ArkéaGreen-Got, ExceetWrap/unwrap partner TR-31 key blocksAES-256 or TDES depending on the Arkéa/partner domain
PVK_VISA (PIN Verification Key, TR-31 V2)ArkeaGreen-Got onlyGenerate and verify Visa PVV for online PIN verificationTDES
PEK_MC_AUTH (PIN Encryption Key, TR-31 P0)Arkea / network domainGreen-Got and network transport boundaryProtect inbound PIN blocks in Mastercard authorization and ATM PIN management flowsTDES
PEK_EXCEET (PIN Encryption Key, TR-31 P0)ArkeaGreen-Got, ExceetProtect outbound manufacturer PIN blocks and personalization payloadsTDES
KCVV (CVV/CVC Key, aka KCVX)ArkéaGreen-Got onlyCVC operations with partners if needed (wrapped by ZMK); not used for internal storage - Green-Got uses KMS ChaCha20-Poly1305 for storageTDES
IMK Set (Issuer Master Keys)ArkéaGreen-Got, card chips (via Exceet)Issuer master keys (IMK-AC TK:10, IMK-IDN TK:14, IMK-SMC TK:12, IMK-SMI TK:11) for cryptograms and secure messaging (wrapped by ZMK)TDES
KAAV (3DS AAV Key, TR-31 M7)ArkéaGreen-Got, Apata3D Secure authentication value validationHMAC-256

Key principle: Green-Got is a key consumer, not a key generator. All payment network keys come from Arkéa.

flowchart TD
    A[ARKea TEST TR-31 Keys
Wrapped under ZMK TEST GGT_GGT A10] --> B[Card Issuance Keys] A --> C[Transaction Keys] A --> D[PIN Management Keys] B --> B1[IMK-AC
Application Cryptograms
TK:10, DES-128] B --> B2[IMK-IDN
ICC Dynamic Number
TK:14, DES-128] B --> B3[IMK-SMC
Secure Messaging Confidentiality
TK:12, DES-128] B --> B4[IMK-SMI
Secure Messaging Integrity
TK:11, DES-128] C --> C1[KAAV
3D-Secure Authentication
TR-31 M7, HMAC-256] C --> C2[KCVV
Card Verification Value
TK:05, DES-128] D --> D1[PEK_EXCEET
Manufacturer PIN transport
TR-31 P0, DES-128] D --> D2[PVK_VISA
PIN verification
TR-31 V2, DES-128] D --> D3[PEK_MC_AUTH
Mastercard PIN transport
TR-31 P0] B1 --> E[Exceet
Card Personalization] B2 --> E B3 --> E B4 --> E D1 --> E C1 --> F[Apata
3DS Provider] C2 --> G[GGT Only] D2 --> G

Storage vs Communication:

3.2 Key import process

Step 1: Receive keys from Arkéa

Arkéa provides keys in TR-31 format (wrapped) or as XOR components:

Step 2: Import ZMK to AWS Payment Cryptography

1. Receive 3 ZMK components from Arkéa in separate files
2. XOR components together: ZMK = Component1 ⊕ Component2 ⊕ Component3
3. Import to AWS Payment Cryptography:
   - Algorithm: TDES_2KEY or TDES_3KEY
   - Usage: TR31_K0_KEY_ENCRYPTION_KEY
   - Exportability: NON_EXPORTABLE
4. Store ZMK ARN for later unwrapping operations

Step 3: Import wrapped keys (PVK, PEKs, KCVX, IMK)

  1. Receive TR-31 wrapped key blocks from Arkéa
  2. Import to AWS Payment Cryptography using ZMK ARN as unwrapping key:
KeyTR-31 Key TypePurpose
PVK_VISAV2Visa PVV generation and verification
PEK_MC_AUTHP0Mastercard authorization and ATM PIN transport
PEK_EXCEETP0Manufacturer communication and chip personalization
KCVXCard verification keyCVC operations with partners if needed (not used for internal storage)
IMKacMAC key, ISO-9797ARQC/ARPC cryptograms
IMKsmiMAC key, ISO-9797Script MAC
IMKsmcMAC key, ISO-9797CVC derivation
IMKidnMAC key, ISO-9797Identifier key (not required)
  1. Store key ARNs for later cryptographic operations

Step 4: Import KAAV key for 3DS

1. Receive HMAC-256 key from Arkéa (same key given to Apata), wrapped under ZMK
2. Import to AWS Payment Cryptography using ZMK ARN as unwrapping key:
   - Algorithm: HMAC_SHA256
   - Key Usage: TR31_M7_HMAC_KEY
   - Key Modes of Use: Verify (and Generate if CBS needs to generate AAVs)
3. Use AWS Payment Cryptography VerifyMac API for AAV validation during 3DS authorization

3.3 Key hierarchy

Two separate key domains:

Domain 1: AWS KMS (Green-Got Internal Storage)

AWS KMS (Managed by Green-Got, generated by AWS)
└─ KEK (Customer Master Key)
   └─ Data Keys (per encryption scope)
      ├─ CHD Data Key (for PANs, cardholder names, expiry)
      └─ SAD Data Key (for PINs, other sensitive authentication data)

Note: All internal storage (PAN, PVV, issuer-side encrypted PIN block, cardholder metadata) uses KMS-backed envelope encryption with ChaCha20-Poly1305 and KMS-generated 256-bit data keys. Arkea payment keys (PVK, PEK, IMK) are only for payment cryptographic operations and transport protection, never for internal storage.

Domain 2: Payment Network Keys (Arkea Key Authority)

Arkea (Key Authority - generates all payment network keys)
├─ ZMK (Zone Master Key) - TDES
│  │
├─ Green-Got Issuer Keys - TDES, wrapped by ZMK
│  │  ├─ PVK_VISA (PIN Verification Key, TR-31 V2)
│  │  ├─ PEK_MC_AUTH (PIN Encryption Key, TR-31 P0)
│  │  └─ KCVX (CVV/CVC Key)
│  │
│  ├─ Shared Keys - TDES, wrapped by ZMK
│  │  ├─ PEK_EXCEET (PIN Encryption Key, TR-31 P0) - shared with Exceet
│  │  └─ IMK Set (per BIN range) - shared with Exceet
│  │     ├─ IMKac (ARQC/ARPC cryptograms)
│  │     ├─ IMKsmi (Script MAC)
│  │     ├─ IMKsmc (CVC derivation)
│  │     └─ IMKidn (Identifier key)
│  │
│  └─ KAAV Key (3DS, TR-31 M7) - HMAC-256, shared with Apata

4. Encryption operations

4.1 Data-at-rest encryption (KMS)

All cardholder data stored in PostgreSQL is encrypted using AWS KMS with envelope encryption (KEK + data key pattern).

Architecture: Background data key caching for performance and cost optimization:

Benefits:

Security: Each encryption still uses a unique nonce (ChaCha20-Poly1305), ensuring ciphertext uniqueness even when reusing the same data key.

Encryption flow

sequenceDiagram
    autonumber
    participant BG as Background Task
    participant Cache as KMS_DATA_KEY_CACHE
(Global, ArcSwap) participant CBS as Core Banking Service participant KMS as AWS KMS participant DB as PostgreSQL Note over BG,Cache: Initialization (once at startup) BG->>KMS: GenerateDataKey(KEK_ARN) KMS->>KMS: Generate data key KMS-->>BG: Plaintext data key (32 bytes)
+ Encrypted data key blob BG->>Cache: Store (plaintext_key, encrypted_key) Note over BG,Cache: Background refresh (every 15 minutes) loop Every 15 minutes BG->>KMS: GenerateDataKey(KEK_ARN) KMS-->>BG: New plaintext + encrypted keys BG->>Cache: Atomic swap (new keys) end Note over CBS,DB: Encryption (synchronous, no KMS call) CBS->>Cache: Get cached keys Cache-->>CBS: (plaintext_key, encrypted_key) CBS->>CBS: Encrypt PAN with cached plaintext key
ChaCha20-Poly1305(plaintext_key, PAN) = encrypted_pan CBS->>CBS: Build envelope:
[encrypted_key_length (4 bytes)][encrypted_key]
[nonce (12 bytes)][encrypted_pan][auth_tag (16 bytes)] CBS->>DB: INSERT INTO pans (pan_encrypted) VALUES (envelope) DB-->>CBS: Stored CBS->>CBS: Zeroize plaintext data from memory Note over DB: Storage format (bytea):
[encrypted_key_length (4 bytes)][cached encrypted_key]
[nonce (12 bytes)][ciphertext][auth_tag (16 bytes)]

Key architecture details:

Storage format: Envelope encryption with KMS data keys. The encrypted data key is stored alongside the ciphertext, enabling decryption. AWS KMS handles KEK rotation transparently via automatic key material rotation.

Decryption flow

sequenceDiagram
    autonumber
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant KMS as AWS KMS

    CBS->>DB: SELECT pan_encrypted FROM pans WHERE pan_id = ?
    DB-->>CBS: Encrypted envelope blob
    
    CBS->>CBS: Parse envelope to extract:
• Encrypted data key length (4 bytes, u32)
• Encrypted data key blob
• Ciphertext (nonce + encrypted data + auth tag) CBS->>KMS: Decrypt(KEK_ARN, encrypted_data_key) KMS->>KMS: Decrypt data key using KEK
(KEK ID from PARAMETERS.aws_kms_encryption_key_id) KMS-->>CBS: Plaintext data key (32 bytes) CBS->>CBS: Decrypt PAN with plaintext data key
ChaCha20-Poly1305-Decrypt(plaintext_key, ciphertext) Note over CBS: Clear PAN available for authorized use:
• Masking for display
• Re-encryption for partner communication CBS->>CBS: Zeroize plaintext data key and clear PAN from memory

Key principle:

Why decryption isn’t cached: Each encrypted blob contains a different encrypted data key (from when it was encrypted). The cache only helps with new encryptions, not decryption of historical data.

4.2 Partner communication (AWS Payment Cryptography)

When sending data to partners, Green-Got must re-encrypt from KMS storage format to the partner’s required format (TDES for Exceet, AES for others).

Re-encryption flow for Exceet (TDES example)

sequenceDiagram
    autonumber
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant KMS as AWS KMS
    participant PayCrypto as AWS Payment Cryptography
    participant Exceet

    Note over CBS: Need to send card data to Exceet for personalization
    
    CBS->>DB: SELECT pan_id, expiry, cardholder_name
FROM cards WHERE card_id = ? DB-->>CBS: Card metadata with pan_id FK CBS->>DB: SELECT pan_encrypted FROM pans
WHERE pan_id = ? DB-->>CBS: KMS-encrypted PAN with key prefix CBS->>KMS: Decrypt(encrypted_data_key) KMS-->>CBS: Plaintext data key CBS->>CBS: Decrypt PAN with data key
Clear PAN available CBS->>CBS: Build Exceet payload
(PAN, expiry, CVC, name) CBS->>PayCrypto: Encrypt with TDES
Key: PEK_EXCEET_ARN
Algorithm: TDES_3KEY, CBC, PKCS7 PayCrypto->>PayCrypto: Encrypt with shared TDES key PayCrypto-->>CBS: TDES-encrypted payload CBS->>CBS: Zeroize clear PAN from memory CBS->>Exceet: Send TDES-encrypted payload (TLS + VPN) Exceet->>Exceet: Decrypt with shared PEK_EXCEET copy Exceet->>Exceet: Personalize card Note over CBS,Exceet: For AES partners (e.g., Mastercard):
Same pattern, but use AES_ZMK_ARN
and Algorithm: AES_256, GCM

Key insight:

5. Transaction flows with cryptographic operations

5.1 EMV chip transaction domain (physical cards)

For detailed transaction-specific flows, see individual documents:

5.2 3D Secure domain (online transactions)

For detailed 3DS flows, see internal 3DS documentation.

High-level flow:

sequenceDiagram
    autonumber
    participant Cardholder
    participant Merchant
    participant Apata as Apata (3DS Provider)
    participant CBS as Core Banking Service
    participant KMS as AWS KMS

    Cardholder->>Merchant: Initiate online purchase
    Merchant->>Apata: 3DS authentication request
    Apata->>Cardholder: Challenge (OTP, biometric)
    Cardholder->>Apata: Complete authentication
    
    Apata->>Apata: Generate AAV using HMAC-256 key from Arkéa
    Apata-->>Merchant: AAV + authentication result
    
    Merchant->>CBS: Authorization request (PAN token, AAV, amount)
    CBS->>KMS: Decrypt PAN from storage (if needed)
    KMS-->>CBS: Clear PAN
    
    CBS->>AWS Payment Cryptography: Validate AAV using HMAC-256 key from Arkéa
    AWS Payment Cryptography-->>CBS: AAV valid
    CBS->>CBS: Check balance, fraud rules
    
    CBS-->>Merchant: Authorization approved
    Merchant-->>Cardholder: Purchase complete

Key points:

5.3 Card issuance and personalization

High-level flow:

sequenceDiagram
    autonumber
    participant User
    participant App
    participant Iframe as Dedicated PIN Iframe
    participant CBS as Core Banking Service
    participant PayCrypto as AWS Payment Cryptography
    participant DB as PostgreSQL
    participant Exceet

    User->>CBS: Request card creation
    CBS->>CBS: Generate PAN, allocate from BIN range
    CBS->>CBS: Generate CVC (3 digits)
    User->>App: Choose PIN for physical card
    App->>Iframe: Open dedicated PIN iframe
    User->>Iframe: Enter PIN
    Iframe->>CBS: Selected PIN over TLS
    CBS->>CBS: Validate PIN rules
    CBS->>CBS: Build ISO-4 PIN block
protected by an ECDH-derived key CBS->>PayCrypto: TranslatePinData(Incoming: ECDH + ISO-4,
Outgoing: PEK_MC_AUTH + Mastercard-required format) PayCrypto-->>CBS: PEK_MC_AUTH PIN block CBS->>PayCrypto: GeneratePinData(VisaPinVerificationValue,
PVK_VISA, PEK_MC_AUTH,
EncryptedPinBlock = PEK_MC_AUTH PIN block) PayCrypto-->>CBS: { VerificationValue: "pvv",
EncryptedPinBlock: "issuer_pin_block" } CBS->>DB: Store encrypted PAN, pvv, issuer_pin_block, CVC, cardholder metadata Note over CBS,Exceet: Only when sending to Exceet CBS->>CBS: Build Exceet payload (PAN, CVC, name, expiry) CBS->>PayCrypto: TranslatePinData(issuer_pin_block,
PEK_MC_AUTH -> PEK_EXCEET + ISO-0) PayCrypto-->>CBS: PEK_EXCEET ISO-0 PIN block CBS->>CBS: Add manufacturer PIN block to payload CBS->>CBS: Encrypt payload with PGP CBS->>Exceet: Send PGP-encrypted card profile CBS-->>User: Card delivery notification Exceet->>Exceet: Decrypt, personalize chip, print card Exceet-->>CBS: Card shipped event

Key points:

6. Key rotation

6.1 KEK rotation (AWS KMS)

Frequency: Annual (automated by AWS KMS)

Process:

  1. AWS KMS automatically rotates KEK yearly
  2. Old KEK versions retained for decrypting existing data
  3. New encryptions use new KEK version
  4. No application changes required (transparent rotation)

Data key rotation impact:

Re-encryption: Optionally re-encrypt all data with new KEK:

1. Schedule maintenance window
2. For each encrypted field in database:
   a. Decrypt with old KEK (KMS handles version automatically)
   b. Re-encrypt with current cached data key (new KEK version)
   c. Update database record
3. Validate re-encryption success
4. Delete old KEK version (after retention period)

Data key cache behavior during rotation:

6.2 Payment network key rotation (Arkéa-provided keys)

Frequency:

Process:

  1. Arkéa draws the replacement TDES TR-31 key set before the end of the current issuance period.
  2. Arkéa distributes replacement key material to Green-Got through the secure key ceremony and partner channels.
  3. Green-Got imports the replacement ZMK or wrapped TR-31 key blocks into AWS Payment Cryptography.
  4. Green-Got validates KCVs, AWS Payment Cryptography key state, enabled key modes, and partner activation dates.
  5. Green-Got updates key ARN references so new card issuance and personalization use the replacement key set after partner rollout.
  6. During the 2-3 month overlap, both the previous and replacement key sets remain available for controlled validation and cutover.
  7. After issuance cutover, the previous TDES key set is no longer used for new card personalization. It remains available only for authorization, validation, PIN, EMV, and card-lifecycle operations tied to cards issued under it.
  8. After the last card issued under the previous key set expires, Green-Got retires the previous key set from AWS Payment Cryptography under change control.

Coordination: Mastercard-side PIN transport and Exceet manufacturing are coordinated separately because PEK_MC_AUTH and PEK_EXCEET are different trust-boundary keys. Exceet receives the relevant personalization-side key material for new card profiles.

6.3 KAAV key rotation (3DS)

Frequency: As dictated by Arkéa and Apata for the 3DS domain.

Process:

  1. Arkéa generates new KAAV key (HMAC-256)
  2. Arkéa distributes to both Green-Got and Apata simultaneously (wrapped under ZMK)
  3. Green-Got imports new key to AWS Payment Cryptography
  4. Apata updates key in their system
  5. Grace period where both old and new keys are valid for validation
  6. After grace period, old key deleted from AWS Payment Cryptography

6.4 Card-personalization PKI expiry tracking

Scope: Mastercard card-personalization PKI expiry date used by Arkéa, Mastercard, and Exceet for production card personalization.

Current validity: Arkéa confirmed that the Mastercard PKI currently signs certificates through the end of 2035. This covers the first Green-Got issuance cycle but does not cover a later renewal cycle that would issue cards expiring after 2035.

Expiry constraint: Green-Got does not issue cards with an expiry date later than the applicable Mastercard PKI validity. Green-Got tracks the applicable PKI expiry date communicated by Arkéa or Exceet and uses it as a constraint for card expiry.

Review timing: Green-Got reviews the applicable PKI expiry around 2030. This provides lead time before the next 3-year issuing-key cycle produces cards whose 5-year validity would extend beyond the end-2035 PKI validity.

Process:

  1. Green-Got confirms the active Mastercard PKI validity window with Arkéa or Exceet before the next issuing-key renewal.
  2. Green-Got records the applicable PKI expiry date in the key and certificate inventory.
  3. Arkéa, Mastercard, and Exceet operate the personalization PKI and related certificate material.
  4. After external PKI renewal by Arkéa, Mastercard, and Exceet, Green-Got obtains the updated applicable expiry date from Arkéa or Exceet and updates the inventory.
  5. Green-Got applies the updated expiry constraint before issuing new cards whose validity extends beyond the previous PKI expiry.

7. Security considerations

7.1 Key security

Key TypeStorageAccess ControlExportabilityRotation
KEK (KMS)AWS KMS HSMIAM policies, MFA requiredNever exportedAnnual (automatic)
Data Keys (cached)In-memory cache (15 min), encrypted by KEK in DBIAM policiesPlaintext zeroized on refreshEvery 15 minutes (automatic)
Data Keys (stored)Encrypted by KEK, stored with each ciphertextIAM policiesDecrypted only for that ciphertextN/A (tied to specific data)
ZMK (AES-256 or TDES)AWS Payment Cryptography HSMIAM policies, MFA requiredNever exportedCurrent production ZMK retained through end-2030 per Arkéa lifecycle
PVK_VISA (TDES)AWS Payment Cryptography HSMIAM policiesNever exported3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total
PEK_MC_AUTH (TDES)AWS Payment Cryptography HSMIAM policiesNever exported3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total
PEK_EXCEET (TDES)AWS Payment Cryptography HSMIAM policiesNever exported3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total
KCVX (TDES)AWS Payment Cryptography HSMIAM policiesNever exported3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total
IMK Set (TDES)AWS Payment Cryptography HSMIAM policiesNever exported3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total
KAAV Key (HMAC-256, TR-31 M7)AWS Payment Cryptography HSMIAM policies, MFA requiredNever exportedPer Arkéa / Apata 3DS replacement schedule

7.2 Clear data handling

Principle: Clear CHD/SAD exists only transiently in CBS memory. All sensitive data is zeroized immediately after use.

Memory security for data keys:

Memory security for cardholder data:

Allowed scenarios for plaintext CHD/SAD:

  1. Masking for display: Decrypt PAN from KMS, extract first 6 + last 4 digits, zeroize clear PAN
  2. Partner communication: Decrypt from KMS, re-encrypt via appropriate HSM (e.g., TDES for Exceet), transmit, zeroize
  3. Authorization validation: Decrypt from KMS only for issuer-side reference data when needed, validate transaction (ARQC, PIN reference data, AAV), zeroize
  4. Database write: Encrypt using cached data key (synchronous), write envelope to DB, zeroize plaintext

Prohibited scenarios:

  1. Logging clear CHD/SAD (even in debug logs)
  2. Passing clear CHD/SAD to non-CDE services
  3. Storing clear CHD/SAD in temporary files or caches
  4. Transmitting clear CHD/SAD over unencrypted channels

Enforcement:

7.3 Known limitations

Encryption context not used as AAD (tracked in CB-1081):

The current implementation has a security limitation where encryption context is only used to authenticate KMS data key wrapping/unwrapping, but not passed as Additional Authenticated Data (AAD) to the ChaCha20-Poly1305 encryption.

Impact: In scenarios where multiple records share the same key_id (which is the case - all records use the same KEK), an attacker who can modify stored database blobs could swap a complete [encrypted_key][ciphertext] envelope from one record to another. The decryption would succeed because:

  1. KMS only authenticates the wrapped data key (not the payload)
  2. ChaCha20-Poly1305 has no AAD binding the ciphertext to specific context

Mitigation in current design:

Required fix: Serialize encryption context and pass as AAD to ChaCha20-Poly1305 encrypt/decrypt operations. This would cryptographically bind each ciphertext to its specific context (e.g., record ID, tenant ID), preventing ciphertext swapping attacks.

Why not fixed yet: The current crypto crate encrypt/decrypt functions don’t support AAD parameters. Adding AAD support requires updating the crypto module API and all call sites, which should be done in a dedicated PR.

See: https://linear.app/green-got/issue/CB-1081/aeadaad-encryption-kmschachapoly

7.4 PCI DSS compliance

RequirementImplementation
Encrypt CHD at restChaCha20-Poly1305 with KEK+data key envelope encryption for all stored card data and issuer-side PIN reference data
Protect keysKEK in KMS HSM (never exported), payment keys in AWS Payment Cryptography HSM
Rotate keysKEK rotated annually (automatic), ZMK retained per Arkéa lifecycle, TDES TR-31 issuing/payment keys replaced after a 3-year issuance period and retained only for non-expired cards
Access controlsIAM policies with least privilege, MFA for HSM access, quarterly reviews
Audit loggingCloudTrail logs all KMS and AWS Payment Cryptography API calls, immutable S3 storage
Network segmentationCDE components in private subnets, restrictive security groups, VPC endpoints for HSMs
No clear PIN storage requiredThe selected physical-card design stores encrypted PVV and a single issuer-side encrypted PIN block retained under PEK_MC_AUTH rather than the clear PIN itself. Transport PIN blocks and cryptograms remain ephemeral only

8. Operational guidelines

8.1 For developers

Encryption best practices:

Application startup:

8.2 For operations

Monitoring:

Key rotation:

Operational procedures:

8.3 For compliance

PCI DSS audit focus areas:

9. Related documentation

For specific data element details, see:

Change History

VersionDateAuthorChanges
1.02026-02-10julian@green-got.comCreate the card-domain cryptography and key-management reference document.
1.12026-04-02julian@green-got.comAdd the cardholder data flow details and supporting architecture clarifications.
1.22026-04-17julian@green-got.comAlign PIN architecture, partner communication flows, and payment cryptographic operations.
1.32026-04-21julian@green-got.comClarify AWS KMS data key wording and storage-encryption terminology.
1.42026-04-30julian@green-got.comAlign PAN lookup HMAC terminology and lifecycle details with the current application-managed design.
1.52026-06-04julian@green-got.comDocument Arkéa ZMK/TR-31 cryptoperiods and Mastercard card-personalization PKI expiry tracking.

3. Cardholder Data Flow

Cardholder Data Flow Diagram

The diagram below shows all cardholder data flows across Green-Got’s Cardholder Data Environment (CDE).

Cardholder Data Flow Diagram

Card

0. Documentation Index

Card Documentation Index

Foundation Documents

Card Components

Authentication Flows

Transaction Flows

Storage

Others

To Be Created

14. Raw Authorization Messages Storage

Raw Authorization Messages Storage

For debugging and investigation purposes, raw ISO 8583 messages received from and sent to Mastercard are stored encrypted in ClickHouse.

ISO 8583 messages may contain SAD and CHD, including PAN, CVC2, and PIN blocks. All messages are encrypted in CBS memory using ChaCha20-Poly1305 with a KMS-generated 256-bit data key (AWS KMS envelope encryption) before leaving the application. Messages are never written to disk or transmitted in cleartext. Memory is zeroized after encryption.

Encrypted messages are transferred to ClickHouse over AWS PrivateLink. Access to the raw message store is restricted via least-privilege roles.

15. Clearing Files Processing

IPM Clearing Files Processing

Clearing files (IPM files) are retrieved by Green-Got from Mastercard via SFTP using Mastercard’s File Exchange solution.

Upon receipt, each file is archived then processed:

Transmission to Arkea

Arkea requires the transmission of IPM clearing files. So clearing files are transmitted to Arkea via SFTP, using a dedicated SFTP server and credentials provided by Arkea. The file is transmitted encrypted with PGP, but the SFTP connection is encrypted using SSH.

Note on settlement

For settlement, Green-Got initiates a funds transfer to the settlement account held at Arkéa. Arkéa is then responsible for the onward settlement with Mastercard. Settlement amounts are aggregated totals; no SAD or CHD is transmitted, processed, or referenced at any point in the settlement flow.

16. ClickHouse Synchronization

ClickHouse Synchronization

PostgreSQL/Aurora data is synchronized to Green-Got’s data warehouse, ClickHouse, using ClickPipes. Data is transferred over AWS PrivateLink.

Green-Got uses the data warehouse for transaction analytics, fraud detection, risk monitoring, and regulatory reporting requiring complex queries and historical data analysis without impacting the operational database.

ClickHouse stores a raw synchronized copy of what is in PostgreSQL. All SAD and CHD fields are transferred and stored encrypted, carrying their encrypted data key alongside the ciphertext, exactly as stored in PostgreSQL. ClickHouse does not hold decryption keys and cannot access plaintext SAD or CHD. For operations requiring decryption of specific records, data is pulled from ClickHouse into the CDE where decryption is performed using AWS KMS.

17. Automatic Billing Updater (ABU)

Automatic Billing Updater (ABU)

Automatic Billing Updater (ABU) is a service provided by Mastercard that requires card issuers to provide updates on cardholder information, such as new card numbers or expiration dates, when a card is reissued or renewed. This helps ensure that recurring payments and subscriptions continue without interruption.

As a card issuer, Green-Got is responsible for implementing the ABU service and providing the necessary updates to Mastercard when cardholder information changes.

In case an account is closed, the card is reissued, or the expiration date is updated, Green-Got will send the updated information to Mastercard through the ABU service.

The CHD transmitted is:

The data is transmitted securely to Mastercard using the PAM (Payment Account Management) API, which requires authentication and encryption to protect the data in transit. Access to the PAM API is restricted to authorized personnel and systems, and all interactions with the API are logged for auditing purposes.

Details of the Mastercard PAM API can be found in the official Mastercard documentation: https://developer.mastercard.com/payment-account-management/documentation/api-basics/

3. PIN

PIN (Personal Identification Number)

This document describes the target PIN design for physical cards: cardholder-selected PIN at card order time, issuer-side online PIN verification based on Visa PVV, issuer-side storage of a reversible encrypted PIN block for change and reveal flows, offline PIN for most POS transactions, online PIN for ATMs, and two distinct PEKs depending on the transport flow. For cryptographic implementation details, see Cryptography & Key Management.

Terminology note:

Key architecture overview:

Issuer-owned persistent data:

AWS Payment Cryptography keys and functions:

Design decision:

1. What is a PIN?

A PIN (Personal Identification Number) is a numeric code used to authenticate the cardholder in card-present flows.

Key characteristics:

PIN origination model:

Clear PIN exposure model:

PIN verification methods:

Purpose:

2. PIN Block Boundaries

A PIN block is a PIN representation formatted according to ISO 9564 and enciphered under a PIN Encryption Key (PEK) or protected by an ECDH-derived key before transport.

Green-Got does not use a single PIN block format across all flows. Each trust boundary has its own key and expected format.

2.1 PIN Block Formats by Boundary

BoundaryKey / protectionPIN block formatStatusUsage
Dedicated PIN iframe to CBSTLS 1.2+ app sessionClear PIN field, not a PIN blockDefinedPIN selection and PIN change form submission
CBS to AWS Payment CryptographyECDH-derived protectionISO-4Defined by AWS Payment Cryptography ECDH flowsInput to TranslatePinData before PVV generation
Mastercard authorization / ATM PIN managementPEK_MC_AUTHTo be confirmed with Mastercard / processorUndefinedOnline PIN authorization, ATM PIN management, stored issuer-side encrypted PIN block
Exceet manufacturingPEK_EXCEET (TR-31 P0, TDES)ISO-0DefinedManufacturer PIN block included in the card personalization payload
EMV chip PIN change scriptIMK-SMI / IMK-SMC secure messagingScheme-defined APDU payloadDefined by EMV profile and issuer script supportDeferred offline PIN synchronization on chip

Design rule:

2.2 PIN Block E2E Flow

This diagram shows the target lifecycle for PIN set, storage, manufacturing distribution, and online verification.

sequenceDiagram
    autonumber
    participant User
    participant App
    participant Iframe as Dedicated PIN Iframe
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant AWS as AWS Payment Cryptography
    participant Exceet
    participant Terminal
    participant Mastercard

    Note over User,Exceet: 1. PIN creation at physical card order time
    User->>App: Choose 4-digit PIN
    App->>Iframe: Open dedicated PIN iframe
    User->>Iframe: Enter chosen PIN
    Iframe->>CBS: Card order + selected PIN over TLS
    CBS->>CBS: Validate PIN rules
(DOB patterns, sequences, repetitions) CBS->>CBS: Build ISO-4 PIN block
protected by an ECDH-derived key CBS->>AWS: TranslatePinData(Incoming: ECDH + ISO-4,
Outgoing: PEK_MC_AUTH + Mastercard-required format) AWS-->>CBS: PEK_MC_AUTH PIN block CBS->>AWS: GeneratePinData(VisaPinVerificationValue,
PVK_VISA, PEK_MC_AUTH,
EncryptedPinBlock = PEK_MC_AUTH PIN block) AWS-->>CBS: { VerificationValue: "pvv",
EncryptedPinBlock: "issuer_pin_block" } CBS->>DB: Store encrypted pvv + encrypted issuer_pin_block CBS->>DB: Set Green-Got online attempts = 3
+ mirrored chip PTC = 3 CBS->>DB: Clear online and chip PIN block statuses CBS->>AWS: TranslatePinData(issuer_pin_block,
PEK_MC_AUTH -> PEK_EXCEET + ISO-0) AWS-->>CBS: PEK_EXCEET ISO-0 PIN block CBS->>CBS: Build PGP payload CBS->>Exceet: Send PGP payload with PIN block Note over Terminal,CBS: 2. Online PIN verification for ATM / rare online PIN flows User->>Terminal: Enter PIN Terminal->>Mastercard: Authorization + encrypted PIN block Mastercard->>Mastercard: Translate to issuer PEK
(PEK_MC_AUTH) Mastercard->>CBS: Authorization + encrypted PIN block CBS->>DB: Fetch encrypted pvv DB-->>CBS: Encrypted pvv CBS->>CBS: Decrypt pvv with KMS CBS->>AWS: VerifyPinData(encrypted_pin_block,
PAN, VisaPinVerification,
PVK_VISA, PEK_MC_AUTH) alt PIN verification successful AWS-->>CBS: HTTP 200 CBS->>DB: Reset Green-Got online attempts to 3 CBS->>DB: Clear online PIN block status CBS-->>Mastercard: Authorization approved else PIN verification failed AWS-->>CBS: HTTP 400 (VerificationFailedException) CBS->>DB: Decrement Green-Got online attempts alt online attempts reach 0 CBS->>DB: Set online PIN block status end CBS-->>Mastercard: Authorization declined end

3. PIN Storage, Verification, and Distribution

3.1 PIN Storage

Purpose: persist the issuer-side data required to verify the PIN online, reveal the PIN in the app, and generate downstream PIN material without storing the clear PIN.

Target design:

Why this is the selected baseline:

Persisted issuer-side data:

Key points:

Dedicated iframe usage:

3.2 PIN Verification

Green-Got supports two verification methods. The terminal and chip decide which one applies for a given transaction.

MethodFrequencyWho verifiesGreen-Got involvementMain use case
Offline PINStandard for POSEMV chipValidates ARQC and mirrors chip PTC when availablePOS transactions in Europe
Online PINStandard for ATMGreen-Got via AWS Payment CryptographyReceives encrypted PIN block and verifies against stored PVVATM withdrawals, ATM services, some fallback flows

3.2.1 Offline PIN Verification (Standard for POS Terminals)

Context: This is the standard CVM for EMV chip POS transactions in Europe. The chip verifies the PIN locally.

Green-Got’s role:

Technical details:

Advantages:

Flow diagram:

sequenceDiagram
    autonumber
    participant User
    participant Terminal
    participant Chip as EMV Chip
    participant Mastercard
    participant CBS as Core Banking Service

    Note over User,Chip: Offline PIN verification
    User->>Terminal: Insert card
    Terminal->>Chip: Request transaction data
    Chip-->>Terminal: Application data
    Terminal->>User: Prompt for PIN
    User->>Terminal: Enter PIN
    Terminal->>Chip: Send PIN

    alt PIN correct
        Chip->>Chip: Verify PIN locally
        Chip->>Chip: Keep PTC at 3
        Chip->>Chip: Generate ARQC
        Chip-->>Terminal: ARQC + DE-55 data
        Terminal->>Mastercard: Authorization + ARQC
        Mastercard->>CBS: Authorization + ARQC
        CBS->>CBS: Validate ARQC
        CBS->>CBS: Read Tag 9F17 when present
        CBS->>DB: Store mirrored chip PTC when present
        CBS-->>Mastercard: Authorization response
    else PIN incorrect
        Chip->>Chip: Decrement PTC
        Chip-->>Terminal: PIN failed + updated PTC
        Terminal->>Mastercard: Authorization + DE-55
        Mastercard->>CBS: Authorization + DE-55
        CBS->>DB: Mirror chip PTC from Tag 9F17
        alt PTC = 0
            CBS->>DB: Set chip PIN block status
        end
        CBS-->>Mastercard: Authorization response
    end

3.2.2 Online PIN Verification (Standard for ATMs)

Context: This is the standard method for ATM withdrawals and ATM services. The issuer verifies the PIN during authorization.

Keys used:

When used:

Verification during transaction:

  1. Terminal or ATM sends an encrypted PIN block.
  2. Mastercard translates that PIN block to the issuer’s authorization PEK.
  3. CBS fetches the stored encrypted PVV and decrypts it with KMS.
  4. CBS calls VerifyPinData with:
    • the encrypted PIN block,
    • PAN,
    • Visa PIN verification attributes,
    • PVK_VISA,
    • PEK_MC_AUTH.
  5. AWS Payment Cryptography:
    • decrypts the inbound PIN block,
    • derives the Visa PVV candidate,
    • compares the result with the stored PVV.

Critical note:

AWS references:

Flow diagram:

sequenceDiagram
    autonumber
    participant User
    participant ATM
    participant Mastercard
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant AWS as AWS Payment Cryptography

    Note over User,ATM: Online PIN verification
    User->>ATM: Insert card
    ATM->>User: Prompt for PIN
    User->>ATM: Enter PIN
    ATM->>Mastercard: Authorization + encrypted PIN block
    Mastercard->>Mastercard: Translate to PEK_MC_AUTH
    Mastercard->>CBS: Authorization + encrypted PIN block

    CBS->>DB: Fetch encrypted pvv
    DB-->>CBS: Encrypted pvv
    CBS->>CBS: Decrypt pvv with KMS
    CBS->>AWS: VerifyPinData(encrypted_pin_block,
PAN, VisaPinVerification,
PVK_VISA, PEK_MC_AUTH) alt PIN verification successful AWS-->>CBS: HTTP 200 CBS->>DB: Reset Green-Got online attempts to 3 CBS->>DB: Clear online PIN block status CBS-->>Mastercard: Authorization approved Mastercard-->>ATM: Approved else PIN verification failed AWS-->>CBS: HTTP 400 (VerificationFailedException) CBS->>DB: Decrement Green-Got online attempts alt online attempts = 0 CBS->>DB: Set online PIN block status + blocked_at end CBS-->>Mastercard: Authorization declined Mastercard-->>ATM: Declined end

Differences from offline PIN:

3.2.3 Magstripe Transactions (International Compatibility)

Context: Magstripe support exists for international compatibility and legacy acceptance.

Supported CVM (Cardholder Verification Method):

CVM methodSupportUse casePIN verification
SignatureYesLegacy US retail and fuel flowsNo PIN
Online PINYesATM and some fallback flowsSame Visa PVV verification as ATM

Notes:

3.3 PIN Encryption Keys (PEK)

Green-Got uses two distinct PEKs. They serve different trust boundaries and must not be conflated.

Mastercard authorization flow:

flowchart LR
    T[ATM / Terminal] -->|PIN block| MC[Mastercard]
    MC -->|PIN block under PEK_MC_AUTH| H[AWS Payment Cryptography]
    H -->|VerifyPinData against PVV| CBS[Core Banking Service]

Exceet manufacturing flow:

flowchart LR
    CBS[Core Banking Service] -->|Issuer PIN block retained under PEK_MC_AUTH| H[AWS Payment Cryptography]
    H -->|TranslatePinData to PEK_EXCEET| P[PEK_EXCEET PIN block]
    P --> E[Exceet]

3.3.1 Mastercard Authorization PEK

Purpose: protect PIN blocks transported in online PIN authorization and ATM PIN management messages between Mastercard and Green-Got.

Used for:

Key property:

Rotation note:

3.3.2 Exceet Manufacturing PEK

Purpose: protect PIN blocks sent to Exceet for manufacturing and personalization.

Used for:

Key property:

3.3.3 Why the Split Matters

4. PIN Lifecycle

4.1 PIN Creation

sequenceDiagram
    autonumber
    participant User
    participant App
    participant Iframe as Dedicated PIN Iframe
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant AWS as AWS Payment Cryptography
    participant Exceet

    User->>App: Order physical card
    App->>Iframe: Open dedicated PIN iframe
    User->>Iframe: Enter chosen PIN
    Iframe->>CBS: Card order + selected PIN over TLS
    CBS->>CBS: Validate PIN rules
    CBS->>CBS: Build ISO-4 PIN block
protected by an ECDH-derived key CBS->>AWS: TranslatePinData(Incoming: ECDH + ISO-4,
Outgoing: PEK_MC_AUTH + Mastercard-required format) AWS-->>CBS: PEK_MC_AUTH PIN block CBS->>AWS: GeneratePinData(VisaPinVerificationValue,
PVK_VISA, PEK_MC_AUTH,
EncryptedPinBlock = PEK_MC_AUTH PIN block) AWS-->>CBS: { VerificationValue: "pvv",
EncryptedPinBlock: "issuer_pin_block" } CBS->>DB: Store encrypted pvv + encrypted issuer_pin_block CBS->>DB: Set Green-Got online attempts = 3
+ mirrored chip PTC = 3 CBS->>DB: Set sync = Synchronized
+ clear online and chip PIN block statuses CBS->>AWS: TranslatePinData(issuer_pin_block,
PEK_MC_AUTH -> PEK_EXCEET + ISO-0) AWS-->>CBS: PEK_EXCEET ISO-0 PIN block CBS->>CBS: Build PGP payload CBS->>Exceet: Send PGP-encrypted personalization payload CBS-->>User: Card order accepted

User choice and validation rules:

Storage result:

Why this design is preferred:

4.2 PIN Verification (Transaction Authorization)

PIN verification follows one of two paths:

  1. Offline PIN verification

    • Standard for POS in Europe
    • Chip verifies the PIN
    • Green-Got validates ARQC and mirrors chip PTC when available
  2. Online PIN verification

    • Standard for ATMs
    • Green-Got verifies the encrypted PIN block against stored Visa PVV
    • Mastercard authorization transport uses PEK_MC_AUTH

Failed attempts:

Method selection:

4.3 PIN Change (App-selected PIN + Deferred Chip Synchronization)

Context: A later PIN change is supported through the mobile app and applied in two stages:

This keeps the anti-pattern validation logic consistent and makes the transition state explicit in the application. During the PendingChipSync period:

sequenceDiagram
    autonumber
    participant User
    participant App
    participant Iframe as Dedicated PIN Iframe
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant AWS as AWS Payment Cryptography
    participant ATM
    participant Mastercard
    participant Chip as EMV Chip

    Note over User,DB: Step 1. User chooses the new PIN in the app
    User->>App: Complete strong authentication
    App->>Iframe: Open dedicated PIN iframe
    User->>Iframe: Enter new PIN
    Iframe->>CBS: New PIN over TLS
    CBS->>CBS: Validate PIN rules
    CBS->>CBS: Build ISO-4 PIN block
protected by an ECDH-derived key CBS->>AWS: TranslatePinData(Incoming: ECDH + ISO-4,
Outgoing: PEK_MC_AUTH + Mastercard-required format) AWS-->>CBS: PEK_MC_AUTH PIN block CBS->>AWS: GeneratePinData(VisaPinVerificationValue,
PVK_VISA, PEK_MC_AUTH,
EncryptedPinBlock = PEK_MC_AUTH PIN block) AWS-->>CBS: { VerificationValue: "new_pvv",
EncryptedPinBlock: "new_issuer_pin_block" } CBS->>DB: Replace active pvv + issuer_pin_block CBS->>DB: Set sync status = PendingChipSync Note over User,Chip: Step 2. User synchronizes chip later at ATM User->>ATM: Insert card ATM->>User: Prompt for new PIN User->>ATM: Enter new PIN ATM->>Mastercard: Authorization + encrypted new PIN block Mastercard->>Mastercard: Translate to PEK_MC_AUTH Mastercard->>CBS: Authorization + new PIN block CBS->>DB: Fetch active pvv + issuer_pin_block + sync status DB-->>CBS: Active pvv + issuer_pin_block + PendingChipSync CBS->>AWS: VerifyPinData(new_pin_block,
PAN, VisaPinVerification,
PVK_VISA, PEK_MC_AUTH) alt new PIN valid CBS->>DB: Reset Green-Got online attempts to 3 CBS->>DB: Clear online PIN block status CBS->>KMS: Decrypt issuer_pin_block storage layer KMS-->>CBS: Return stored issuer_pin_block CBS->>AWS: GenerateMacEmvPinChange(issuer_pin_block,
IMK-SMI, IMK-SMC, PAN) AWS-->>CBS: Issuer script MAC + encrypted PIN script data CBS-->>Mastercard: Response + issuer script Mastercard-->>ATM: Response + issuer script ATM->>Chip: Execute issuer script alt script execution successful Chip-->>ATM: PIN updated CBS->>DB: Set sync status = Synchronized CBS->>DB: Set mirrored chip PTC = 3 CBS->>DB: Clear chip PIN block status else script execution failed Chip-->>ATM: Script failed CBS->>DB: Keep sync status = PendingChipSync end else new PIN invalid AWS-->>CBS: VerificationFailedException CBS->>DB: Decrement Green-Got online attempts alt online attempts = 0 CBS->>DB: Set online PIN block status + blocked_at end end

Key points:

AWS references:

4.4 PIN Unlock (Online Counter vs Chip PTC)

Context: PIN blocking is split between the Green-Got online PIN block status and the chip PIN block status.

sequenceDiagram
    autonumber
    participant User
    participant App as Mobile App
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    
    Note over User,CBS: Step 1. Unlock intent registered in the app
    User->>App: Complete strong authentication
    User->>App: Select "Unlock PIN"
    App->>CBS: Unlock request
    CBS->>DB: Reset Green-Got online attempts to 3
    CBS->>DB: Clear online PIN block status
    CBS-->>App: Online PIN unblocked

    Note over CBS,DB: Chip PIN block status remains unchanged

Key points:

4.5 PIN Reveal

Context: Reveal PIN is supported for physical cards through a dedicated iframe flow. The iframe is the only app component authorized to request and display the clear PIN.

sequenceDiagram
    autonumber
    participant User
    participant App as Mobile App Shell
    participant Iframe as Dedicated PIN Reveal Iframe
    participant CBS as Core Banking Service
    participant DB as PostgreSQL
    participant AWS as AWS Payment Cryptography

    User->>App: Select "Reveal PIN"
    App->>User: Require step-up authentication before opening reveal section
    User->>App: Complete step-up authentication
    App->>Iframe: Open dedicated PIN reveal iframe
    Iframe->>User: Show confirmation + disclaimer
    User->>Iframe: Confirm display
    Iframe->>CBS: Reveal PIN request
    CBS->>DB: Fetch encrypted issuer PIN block
    DB-->>CBS: Encrypted issuer PIN block
    CBS->>CBS: Decrypt issuer PIN block at rest layer
    CBS->>AWS: TranslatePinData(PEK_MC_AUTH -> CBS-controlled reveal context)
    AWS-->>CBS: Protected reveal response
    CBS->>CBS: Recover clear PIN in controlled server memory
    CBS-->>Iframe: Server-rendered PIN display
    Iframe-->>User: Display clear PIN

    Note over App,Iframe: The app shell never receives the clear PIN

Key points:

5. Used In

PIN authentication is used in the following flows:

PIN is not used in:

6. Security Considerations

6.1 PCI DSS Requirements

RequirementImplementation
PIN never in clear text in issuer storageThe target design stores encrypted Visa PVV and an encrypted issuer PIN block, not the clear PIN itself
Protected during transportDedicated iframe over TLS for app set/change and reveal display; dedicated PEKs for network and manufacturer PIN blocks
Encrypted at restpin_pvv, encrypted_pin_block, and card security state encrypted at rest with KMS
Strong key separationPVK_VISA, PEK_MC_AUTH, PEK_EXCEET, and IMKs remain separated by use case
Verification without exposureOnline PIN verification occurs entirely inside AWS Payment Cryptography
No exported payment keysPEKs, PVKs, and IMKs remain HSM-resident in the target design
AuditabilityCloudTrail and issuer logs capture HSM calls without exposing PIN material

6.2 Attack Mitigations

7. Related Concepts

7.1 Comparison with Other Authentication Values

TermDescriptionAlgorithm / key familyUse case
PIN4-digit cardholder secretCardholder knowledge factorCardholder authentication
PVVVisa PIN Verification ValueTR31_V2_VISA_PIN_VERIFICATION_KEYIssuer-side online PIN verification baseline
Issuer Encrypted PIN BlockReversible issuer-side PIN artifactPEK_MC_AUTH (TR-31 P0)Reveal PIN, EMV PIN change preparation, manufacturer translation
PIN BlockProtected PIN data in transitPEK_MC_AUTH for Mastercard-required format; PEK_EXCEET (TR-31 P0) for Exceet ISO-0Online transport or manufacturing distribution
ARQCEMV application request cryptogramIMKac-derivedChip transaction validation
AAV3DS authentication valueKAAV (TR-31 M7) / HMAC-256E-commerce authentication

7.2 Key Hierarchy

Green-Got Internal Storage Keys
└── KEK + data keys (AWS KMS)
    └── Encrypt pin_pvv, encrypted_pin_block, and issuer-side card security state

Arkea / network payment keys in AWS Payment Cryptography
├── PVK_VISA (TR31_V2_VISA_PIN_VERIFICATION_KEY)
│   └── Generate / verify Visa PVV data
├── PEK_MC_AUTH (TR-31 P0)
│   └── Mastercard authorization and ATM PIN transport; issuer-side encrypted PIN block retention
├── PEK_EXCEET (TR-31 P0)
│   └── Exceet manufacturing and personalization flows
├── IMK-SMI / IMK-SMC
│   └── EMV issuer scripts for offline PIN change
└── ECDH key agreement key
    └── CBS-controlled reveal PIN transport protection

See Cryptography & Key Management - Section 3 for the broader key hierarchy and import model.

4. PAN

PAN Lifecycle

This document describes the target design for Primary Account Number (PAN) generation, storage, assignment, and reuse in the Green-Got card issuing system. Implementation is pending.

Overview

The PAN is the cardholder data element that identifies the issuer and cardholder account. Green-Got generates PANs from its available Mastercard BINs and stores them in a pre-generated inventory. A card references a PAN record when the card is issued.

Each PAN record contains:

flowchart TD
    Bin["Mastercard BIN"] --> Generate["Generate account numbers"]
    Generate --> Luhn["Calculate Luhn check digit"]
    Luhn --> Shuffle["Randomly shuffle generated PANs"]
    Classify["Assign scope
standard / ephemeral / test"] Classify --> Protect["Generate keyed HMAC lookup value
Encrypt PAN at rest"] Protect --> Inventory["Store in PAN inventory
status: Available"] Inventory --> Assign{"Card issuance request"} Assign -->|"matching BIN + scope"| Card["Reference PAN from card
status: Assigned"] Assign -->|"test scope"| Test["Use for test or certification
never assigned to live cards"] Card --> Expire["Card expires / is replaced / is cancelled"] Expire --> Lock["Lock PAN
status: Locked
available_at + applicable lock period"] Lock --> Review{"Eligible for reuse?"} Review -->|"no active token, dispute,
fraud case, or restriction"| Inventory Review -->|"fraud / compromise / partner restriction"| Retire["Retire PAN
status: Retired"]

PAN Format

Green-Got PANs use the standard 16-digit structure:

Each BIN therefore provides up to 10,000,000 possible PAN values. Green-Got makes the BIN range available for issuance, except for PAN ranges explicitly reserved for testing, certification, partner validation, or scheme-required segregation.

Inventory Generation

PANs are generated ahead of card issuance and inserted into a single PAN inventory. Each PAN references its BIN and carries a scope so that capacity, product usage, and lifecycle controls remain auditable without creating separate pool structures for each BIN.

The generation process is:

  1. Select the BIN.
  2. Generate every 7-digit account-number value from 0000000 to 9999999.
  3. Calculate the Luhn check digit for each BIN and account-number pair.
  4. Build the full 16-digit PAN.
  5. Randomly shuffle the generated PAN list before insertion so issued PANs are not sequential.
  6. Generate a keyed HMAC-SHA256 lookup value for each PAN.
  7. Encrypt the PAN before storage.
  8. Insert the PAN record with its BIN, scope, status, availability timestamp, and creation timestamp.

PAN assignment uses the pre-generated inventory instead of generating a PAN at card creation time. This makes available capacity explicit and allows PAN usage to be monitored before issuance is blocked.

PAN Scopes

Green-Got maintains one PAN inventory and differentiates PAN usage with a scope field:

ScopePurpose
StandardPANs used for ordinary physical and virtual card issuance.
EphemeralPANs reserved for short-lived or one-time card products.
TestPANs reserved only for test, certification, and controlled non-production or partner validation scenarios.

The test scope is carved out from the beginning of each generated range. These PANs are never assigned to live customer cards.

The ephemeral scope is reserved for temporary card products. Ephemeral PANs follow the same encryption, HMAC lookup, assignment, and audit controls as standard PANs, but they use a shorter lock period because their intended use period is shorter and reuse pressure is higher. The exact ephemeral lock period is defined separately from the standard PAN lock period and must remain long enough to cover authorization, clearing, settlement, dispute, fraud-monitoring, and partner-processing windows for the product.

Lookup And Keyed HMAC Values

Each PAN receives a keyed HMAC-SHA256 lookup value when it is created. This lookup value is generated from the full PAN with a Green-Got-managed HMAC key and stored alongside the encrypted PAN record.

The keyed HMAC lookup value exists to support deterministic lookup without decrypting the PAN. When an external message contains a PAN, Green-Got computes the same keyed HMAC-SHA256 value and uses it to find the matching PAN record. The keyed HMAC lookup value is not used as a public identifier and is not returned in customer-facing APIs.

Customer-facing and internal non-CDE references use separate non-sensitive identifiers. APIs and logs use card identifiers, PAN identifiers, or masked PAN values, not full PANs.

HMAC Key Rotation

PAN lookup HMAC rotation uses two dated lookup slots in the PAN inventory:

During normal operation, the most recently updated slot contains the lookup values generated with the active PAN lookup HMAC key. The older slot remains available as the target slot for the next HMAC key during rotation.

The application loads two protected Parameter Store values at startup:

During a rotation, the worker identifies the oldest lookup slot for each PAN record by comparing pan_hmac1_updated_at and pan_hmac2_updated_at. It decrypts the PAN in application memory, computes the keyed HMAC-SHA256 lookup value with the secondary key, writes the value to the oldest slot, updates that slot’s timestamp, and records the update result. The worker processes the PAN inventory in bounded batches so database writes, PAN decryption, and HMAC generation remain controlled.

The target slot is determined from timestamps, not from whether a slot contains a value. This matters because both lookup slots contain values after completed rotations. The timestamp tells the rotation worker which slot carries the oldest key generation and can safely be replaced by the new one.

During the rotation window, the application computes keyed HMAC lookup values with both loaded keys and matches against both lookup slots. After verification is complete and the previous key is retired, the application keeps only the active key in memory. The lookup query remains compatible with both database slots; the most recently updated slot is the expected match.

Encryption At Rest

PANs are encrypted before storage using the shared Encrypted data pattern used across the cardholder environment.

The storage encryption model is:

This aligns PAN storage with the cardholder environment cryptography model described in 2. Cryptography & Key Management.

Lifecycle Statuses

PAN records move through a controlled lifecycle:

StatusMeaning
Reserved for testPAN is reserved for test or certification usage and is never assigned to a live customer card.
AvailablePAN is assignable to a new card after its available_at timestamp has passed.
AssignedPAN is referenced by an active card or a card in issuance.
LockedPAN is not assignable after card expiry, cancellation, replacement, or closure.
RetiredPAN is permanently removed from future assignment because of fraud, dispute, operational risk, partner instruction, or scheme requirement.

PAN assignment selects from records where the BIN and scope match the requested card product, status is Available, and available_at is less than or equal to the current timestamp. Available PANs are selected in FIFO order by available_at and creation timestamp within the relevant BIN and scope.

Expiry And Reuse

When a card expires or is replaced, the PAN moves to Locked and receives a future available_at timestamp. During the lock period, the PAN remains non-assignable.

PCI DSS does not define a specific minimum waiting period before reassigning an expired or cancelled PAN. PCI SSC guidance states that expired, cancelled, or otherwise invalid PANs remain subject to PCI DSS unless the organization documents that the PAN is inactive or disabled and no longer poses fraud risk to the payment system: PCI SSC FAQ Article 1038: Does PCI DSS apply to hot cards, expired, cancelled, or invalid payment account numbers?.

Green-Got uses a conservative two-year lock period before a previously assigned standard PAN becomes eligible for reassignment. Ephemeral PANs use a shorter lock period, defined per ephemeral card product, because they are designed for short-lived usage. The PAN is reassigned only after the applicable lock period has passed and the PAN has no active card, active tokenization dependency, open dispute, chargeback, fraud case, or partner restriction attached to it.

PANs involved in fraud, confirmed compromise, unresolved disputes, or partner-directed blocking are moved to Retired instead of returning to the available inventory.

Lock Periods

PAN lock periods are defined by scope:

ScopeLock period
StandardTwo years after card expiry, replacement, cancellation, or closure.
EphemeralShorter product-specific lock period, validated against clearing, settlement, dispute, fraud-monitoring, and partner-processing windows before production use.
TestNot assigned to live cards; reuse is managed by test and certification procedures.

PCI DSS does not prescribe these lock-period lengths. Green-Got sets them as issuer lifecycle controls and reviews them against Mastercard, partner, operational, fraud, and QSA requirements before launch.

Capacity Monitoring

PAN inventory usage is monitored per BIN and per scope. Green-Got tracks:

The operating capacity metric is:

Usage (%) = (assigned PANs + locked PANs + retired PANs) / total generated PANs * 100

Green-Got monitors BIN and scope utilization so issuance capacity is visible before available inventory is exhausted. When available capacity approaches operational limits, Green-Got requests or activates additional BIN capacity before card issuance is constrained.

Security Considerations

PAN inventory management follows these controls:

6. 3DS

3DS Authentication Flow

The diagram below shows the authentication flow for a 3DS challenge.

    sequenceDiagram
    autonumber
    participant ch as Cardholder
    participant 3ds as 3DS Server
    participant ds as Directory Server
    participant acs as Apata ACS
    participant issuer as Issuer
    ch->>3ds: Initiate transaction
    3ds->>ds: AReq
    ds->>acs: AReq
    acs->>issuer: Card Link request
    issuer->>acs: Card details
    acs->>acs: Evaluate Risk Profile - challenge required
    acs->>ds: ARes (transStatus C)
    ds->>3ds: ARes (transStatus C)
    3ds->>acs: CReq
    acs->>ch: Deliver OTP and render Challenge Interface
    ch->>acs: Submit OTP
    acs->>acs: Verify OTP
    acs->>ds: RReq
    ds->>3ds: RReq
    3ds->>ds: RRes
    ds->>acs: RRes
    acs->>ch: Redirect to merchant
    opt Finalised Event
        acs->>issuer: Finalised Event notification
    end

Glossary

DS - Directory Server. The payment scheme infrastructure (e.g. Visa, Mastercard) that routes 3DS messages between merchants and the ACS.

ACS - Access Control Server is the system operated by a card issuer that handles authentication requests for 3DS transactions.

AReq - Authentication Request that is created by the 3DS Server and forwarded through the Directory Server to the Apata ACS containing transaction details for authentication.

ARes - Authentication Response. A message returned from the ACS to the Directory Server indicating whether the transaction is authenticated, rejected, or requires a challenge.

transStatus - A field in the ARes message that indicates the outcome of the authentication request. C stands for Challenge Required.

CReq/CRes - Challenge Request/Response. Messages exchanged between the ACS and the cardholder during a challenge flow, typically involving an OTP.

RReq/RRes - Result Request/Response. Messages exchanged between the ACS and the Directory Server after the challenge is completed, indicating the final outcome of the authentication.

Source

Source from Apata Explanation of the different terms

Card Link Request

When a challenge is required, Apata sends Green-Got a Card Link Request via webhook. Green-Got receives the PAN in a format encrypted with AES-256-GCM, avoiding transmission of the PAN in plaintext.

Green-Got decrypts the PAN in memory, generates the associated keyed HMAC-SHA256 lookup value with the startup-loaded PAN lookup HMAC key, retrieves the card in the database, then responds to Apata with the data required to process the challenge.

Green-Got responds per the Apata documentation with non-sensitive card metadata:

No SAD or CHD is returned in this response.

Green-Got stores a keyed HMAC-SHA256 PAN lookup value dedicated to internal card lookup. Apata tooling refers to the card through externalId, which Green-Got sets to the existing card.id; Delegate SCA and Finalised Event processing use that value to link Apata challenges back to the card without exposing the PAN.

flowchart TD
    classDef error fill:#fde8e8,stroke:#e53e3e,color:#9b2335
    classDef success fill:#e6ffed,stroke:#38a169,color:#276749
    classDef terminal fill:#fff5f5,stroke:#fc8181,color:#c53030
    classDef process fill:#ebf8ff,stroke:#4299e1,color:#2c5282
    START([AReq received]) --> CL[Apata sends Card Link request to Green-Got]:::process
    CL --> RESP{Green-Got responds in time?}
    RESP -->|No| ERR([error: webhook_call_failed]):::error
    RESP -->|Yes| CP{cardProgramId returned?}
    CP -->|Yes| CPID[Use specified Card Program]:::process
    CP -->|No| BIN[Use BIN range default Card Program]:::process
    CPID --> STORE{Storage Mode}
    BIN --> STORE
    STORE -->|Enrol| EN[Store as Enrolment]:::process
    STORE -->|Temp| TMP[Use for this transaction only]:::process
    STORE -->|"Temp With Backup"| BKP["Use for this transaction,
save backup copy"]:::process EN --> RISK[Evaluate Risk Profile]:::process TMP --> RISK BKP --> RISK RISK --> OUT{Outcome} OUT -->|Frictionless| SUC([SUCCEEDED · Finalised Event]):::success OUT -->|Challenge| CHAL([Challenge Flow · Finalised Event]):::process OUT -->|Reject| REJ([REJECTED · Finalised Event]):::terminal

Authorization with AAV Verification

After a successful 3DS authentication, the ACS generates an AAV (Accountholder Authentication Value) using the KAAV (Key for AAV Computation). The AAV travels with the authorization request through Mastercard to Green-Got, which verifies it using its own KAAV as the issuer.

sequenceDiagram
    autonumber
    participant mer as Merchant
    participant acq as Acquirer
    participant mc as Mastercard
    participant issuer as Green-Got (Issuer)

    Note over mer: 3DS authentication complete — AAV received from ACS
    mer->>acq: Authorization request (+ AAV in DE 48)
    acq->>mc: Authorization request (+ AAV)
    mc->>issuer: Forward authorization request (+ AAV)

    Note over issuer: Verify AAV using KAAV
    alt AAV valid — transaction is authenticated
        issuer->>mc: Authorization response (approved)
        mc->>acq: Authorization response (approved)
        acq->>mer: Authorization approved
    else AAV invalid or missing
        issuer->>mc: Authorization response (declined)
        mc->>acq: Authorization response (declined)
        acq->>mer: Authorization declined
    end

Glossary

AAV - Accountholder Authentication Value. A cryptogram generated by the ACS after successful 3DS authentication and carried in the Mastercard authorization message (DE 48, SE 43). It proves the transaction was genuinely authenticated.

KAAV - Key for AAV Computation. A key derived from Green-Got’s issuer master key. It is used by the ACS to compute the AAV and by Green-Got to verify it during authorization, closing the cryptographic proof of authentication.

DE 48, SE 43 - Data Element 48, Subelement 43 in the ISO 8583 authorization message. Mastercard uses it to carry additional private-use data, including the AAV.

8. EMV Transactions

EMV Transactions

This document explains EMV chip transactions for Mastercard network, covering both chip contact and contactless (NFC) interfaces. Both interfaces use the same EMV chip and cryptography, the only difference is the physical communication method.

Terminology:


1. Introduction

Green-Got cards contain an EMV chip that supports two transaction interfaces:

  1. Chip Contact: Physical insertion into a card reader with electrical connectors (ISO 7816)
  2. Contactless (NFC): Wireless communication using Near Field Communication (ISO 14443)

Key Point: Both interfaces use the same EMV chip and same cryptographic protocols. The difference is purely in how the terminal communicates with the chip (physical connectors vs. NFC radio).


2. Contactless Transaction Limits

Europe (Mastercard):

When limits exceeded:

Note: Green-Got also enforces daily/monthly spending limits and maximum transaction amounts configured per card.


3. Transaction Comparison

AspectChip ContactContactless (< €50)Contactless + PIN (≥ €50)
InterfacePhysical connectorsNFC wirelessNFC wireless + PIN
InsertionFull insertionTap onlyTap only
PIN requiredYes (standard)NoYes
Mastercard limitNo hard limit€50No hard limit
Consecutive limitNone5 transactionsResets counter
Cumulative limitNone€150Resets cumulative
ARQC generation✅ Yes✅ Yes✅ Yes
Cryptographic securityFullFullFull
CVMOffline PINNo CVMOffline PIN*
Common useATM, high-valueRetail, transportModern retail terminals
Counter resetYesNoYes

*Note: Contactless PIN may use online PIN verification on some terminals, depending on terminal capabilities and card configuration.


4. Transaction Flows

4.1 Contactless Transaction (< €50, No PIN)

Most common contactless flow for quick, low-value payments.

sequenceDiagram
    autonumber
    participant User
    participant Terminal as POS Terminal
    participant Chip as EMV Chip (NFC)
    participant Mastercard
    participant CBS as Core Banking Service
    participant AWS as AWS Payment Cryptography

    Note over User,Terminal: Contactless < €50 (No PIN)
    
    User->>Terminal: Tap card (NFC)
    Terminal->>Chip: Send transaction data (amount, date, merchant)
    
    Note over Chip: No CVM required (< €50)
Generate ARQC Chip-->>Terminal: ARQC + EMV data (DE-55) Terminal->>User: Transaction complete User->>Terminal: Remove card Terminal->>Mastercard: Authorization request + DE-55 Mastercard->>CBS: Authorization request (ISO 8583) CBS->>CBS: Parse DE-55 (ARQC, ATC, CVM Results) Note over CBS,AWS: Validate ARQC CBS->>AWS: VerifyAuthRequestCryptogram(ARQC, TransactionData, IMKac, PAN, ATC) AWS-->>CBS: HTTP 200 (ARQC valid) CBS->>CBS: Validate CVM = "No CVM"
Check balance, limits, fraud alt Approved CBS->>AWS: GenerateMac(ARPC data, IMKac, ATC) AWS-->>CBS: ARPC CBS-->>Mastercard: Approved (+ ARPC) Mastercard-->>Terminal: Approved Terminal-->>User: Approved ✅ else Declined CBS-->>Mastercard: Declined (reason code) Mastercard-->>Terminal: Declined Terminal-->>User: Declined ❌ end

Key Points:


4.2 Chip Contact Transaction with PIN

Standard flow for inserted card transactions at POS terminals and high-value purchases.

Note: ATMs use a different flow with online PIN verification (see 3_pin.md section 3.2.2), not this offline PIN flow.

sequenceDiagram
    autonumber
    participant User
    participant Terminal as POS Terminal
    participant Chip as EMV Chip (Contact)
    participant Mastercard
    participant CBS as Core Banking Service
    participant AWS as AWS Payment Cryptography

    Note over User,Chip: Chip Contact with PIN
    
    User->>Terminal: Insert card
    Terminal->>Chip: Power on, request application
    
    Terminal->>User: Prompt "Enter PIN"
    User->>Terminal: Enter PIN (4 digits)
    
    Terminal->>Chip: Send transaction data + encrypted PIN
    
    Note over Chip: Verify PIN locally (offline)
Generate ARQC Chip-->>Terminal: ARQC + EMV data (DE-55)
CVM Results: "Offline PIN successful" Terminal->>Mastercard: Authorization request + DE-55 Mastercard->>CBS: Authorization request Note over CBS,AWS: Validate ARQC (proves PIN verified) CBS->>AWS: VerifyAuthRequestCryptogram(ARQC, TransactionData, IMKac, PAN, ATC) AWS-->>CBS: HTTP 200 (ARQC valid) CBS->>CBS: Validate CVM = "Offline PIN"
Check balance, limits, fraud alt Approved CBS->>AWS: GenerateMac(ARPC data, IMKac, ATC) AWS-->>CBS: ARPC CBS-->>Mastercard: Approved (+ ARPC) Mastercard-->>Terminal: Approved Terminal-->>User: Approved ✅
Remove card else Declined CBS-->>Mastercard: Declined (reason code) Mastercard-->>Terminal: Declined Terminal-->>User: Declined ❌
Remove card end Note over Chip,CBS: Green-Got NEVER receives PIN
Only validates ARQC

Key Points:


4.3 Contactless with PIN (≥ €50)

Flow for contactless transactions requiring PIN on modern terminals.

sequenceDiagram
    autonumber
    participant User
    participant Terminal as POS Terminal
    participant Chip as EMV Chip (NFC)
    participant Mastercard
    participant CBS as Core Banking Service
    participant AWS as AWS Payment Cryptography

    Note over User,Terminal: Contactless ≥ €50 with PIN
    
    User->>Terminal: Tap card (NFC)
    Terminal->>Chip: Send transaction data (amount ≥ €50)
    
    Terminal->>User: Prompt "Enter PIN"
    User->>Terminal: Enter PIN
    
    Note over Chip: Verify PIN (contactless)
Generate ARQC Chip-->>Terminal: ARQC + EMV data (DE-55)
CVM Results: "Offline PIN successful" Terminal->>Mastercard: Authorization request + DE-55 Mastercard->>CBS: Authorization request CBS->>AWS: VerifyAuthRequestCryptogram(ARQC, TransactionData, IMKac, PAN, ATC) AWS-->>CBS: HTTP 200 (ARQC valid) CBS->>CBS: Validate CVM = "Offline PIN"
Check balance, limits, fraud alt Approved CBS->>AWS: GenerateMac(ARPC data, IMKac, ATC) AWS-->>CBS: ARPC CBS-->>Mastercard: Approved (+ ARPC) Mastercard-->>Terminal: Approved Terminal-->>User: Approved ✅ else Declined CBS-->>Mastercard: Declined Mastercard-->>Terminal: Declined Terminal-->>User: Declined ❌ end

Key Points:


5. DE-55 Structure (ICC Related Data)

DE-55 (Data Element 55) contains ICC Related Data—all EMV tags sent from the chip to the issuer.

Format: TLV (Tag-Length-Value) encoding as per EMV Book 3

5.1 ARQC (Application Request Cryptogram)

ARQC is a cryptographic signature generated by the EMV chip to prove:

  1. Card authenticity: Only a genuine chip with the correct IMKac key can generate valid ARQC
  2. Transaction integrity: Transaction data (amount, date, merchant) is cryptographically bound to ARQC
  3. Anti-replay: Each ARQC is unique (uses incrementing ATC)

ARQC Generation (by EMV Chip):

1. Derive session key:
   Session Key = DeriveKey(IMKac, PAN, ATC)

2. Generate ARQC:
   ARQC = MAC(Transaction Data || Card Data || Terminal Data, Session Key)

ARQC Validation (by Green-Got):

  1. Extract ARQC, ATC, transaction data from DE-55
  2. Call AWS Payment Cryptography: VerifyAuthRequestCryptogram(ARQC, TransactionData, IMKac, PAN, ATC)
  3. AWS derives session key, calculates expected ARQC, compares with received ARQC
  4. Returns HTTP 200 if valid, HTTP 4xx if invalid

5.2 Key EMV Tags in Authorization Request

Required in Authorization Request/0100:

TagNameLengthDescription
9F26Application Cryptogram (AC)8 bytesARQC - Authentication cryptogram proving card authenticity
9F27Cryptogram Information Data1 byteCryptogram type (ARQC/TC/AAC)
9F36Application Transaction Counter (ATC)2 bytesAnti-replay counter, increments each transaction
9F34CVM Results3 bytesMANDATORY - Cardholder Verification Method result
95Terminal Verification Results (TVR)5 bytesTerminal verification flags (PIN, offline data auth, etc.)
9F37Unpredictable Number4 bytesTerminal random number (prevents pre-computation)
9F10Issuer Application Data (IAD)1-32 bytesIssuer-specific data (mandatory if chip provides it)
9ATransaction Date3 bytesYYMMDD format
9CTransaction Type1 bytePurchase (0x00), Cash withdrawal (0x01), Refund (0x20), etc.
9F02Amount Authorized6 bytesTransaction amount in minor currency units (e.g., cents)

Common Optional Tags:

TagNameLengthDescription
5F2ATransaction Currency Code2 bytesISO 4217 currency code (e.g., 0978 = EUR)
82Application Interchange Profile (AIP)2 bytesCard capabilities (SDA/DDA/CDA support)
9F1ATerminal Country Code2 bytesISO 3166 country code

5.3 CVM Results (Tag 9F34) - MANDATORY

Format: 3 bytes

Common values:

ScenarioCVM Results (Hex)Meaning
Contactless < €503F 00 00No CVM performed
Chip contact + PIN (success)02 03 00Offline PIN plaintext, successful
Chip contact + PIN (failed)02 03 01Offline PIN plaintext, failed
Online PIN (success)01 03 00Online PIN, successful
Signature1F 03 00Signature (paper), verified

Green-Got validation:

5.4 ATC (Application Transaction Counter)

ATC is a 2-byte counter maintained by the EMV chip that prevents replay attacks.

How ATC prevents replay:

ATC Storage:

Edge cases:

5.5 ARPC (Authorization Response Cryptogram)

ARPC is a cryptographic signature generated by Green-Got (via AWS Payment Cryptography) and sent back to the chip to prove response authenticity.

When generated:

ARPC proves:

Generation:


6. CVM (Cardholder Verification Method)

CVM is the method used to verify the cardholder is the legitimate card owner.

Common CVM methods:

  1. Offline PIN: PIN verified by chip (standard for chip contact POS)
  2. Online PIN: PIN verified by issuer via Visa PVV (standard for ATMs)
  3. No CVM: No verification (contactless < €50)
  4. Signature: Cardholder signs receipt (legacy, rarely used in Europe)
  5. CDCVM (Consumer Device CVM): Biometric on mobile wallet (Apple Pay, Google Pay)

CVM by Transaction Type:

Transaction TypeTypical CVM
Contactless < €50No CVM
Contactless ≥ €50 (modern)Offline PIN (contactless)
Contactless ≥ €50 (fallback)Offline PIN (chip contact)
Chip contact purchaseOffline PIN
ATM withdrawalOnline PIN (verified via Visa PVV)
Mobile wallet (Apple Pay)CDCVM (Face ID/Touch ID)
Online transaction (CNP)CVV2 + 3DS (not EMV CVM)

7. Green-Got Validation Process

Authorization validation flow:

flowchart TD
    Start[Receive Authorization Request] --> Parse[Parse DE-55]
    Parse --> Card[Retrieve Card Record]
    Card --> Status{Card Active?}
    Status -->|No| Decline1[Decline: Card Not Active]
    Status -->|Yes| ARQC[Validate ARQC via AWS]
    
    ARQC --> ARQCValid{ARQC Valid?}
    ARQCValid -->|No| Decline2[Decline: Invalid ARQC
Possible fraud/cloned card] ARQCValid -->|Yes| ATC[Validate ATC] ATC --> ATCValid{ATC > Last Known?} ATCValid -->|No| Decline3[Decline: Replay Attack] ATCValid -->|Yes| CVM[Validate CVM] CVM --> CVMValid{CVM Appropriate?} CVMValid -->|No| Decline4[Decline: CVM Required] CVMValid -->|Yes| Balance[Check Balance] Balance --> BalanceOK{Sufficient?} BalanceOK -->|No| Decline5[Decline: Insufficient Funds] BalanceOK -->|Yes| Limits[Check Daily/Monthly Limits] Limits --> LimitsOK{Within Limits?} LimitsOK -->|No| Decline6[Decline: Limit Exceeded] LimitsOK -->|Yes| Fraud[Fraud Detection] Fraud --> FraudOK{Score OK?} FraudOK -->|No| Decline7[Decline: Fraud Suspected] FraudOK -->|Yes| Approve[Approve Transaction] Approve --> ARPC[Generate ARPC optional] ARPC --> Log[Log Authorization] Log --> Response[Return Response]

Decline Reasons:

Green-Got uses standard ISO 8583 response codes as defined by Mastercard authorization specifications. Common decline reasons:

Reason CodeDescriptionUser Message
51Insufficient fundsInsufficient balance
54Card expiredCard expired
57Transaction not permittedTransaction not allowed
61Exceeds withdrawal limitDaily limit exceeded
65Exceeds withdrawal frequencyToo many transactions
75PIN tries exceededPIN blocked (online counter or chip PTC exhausted)
89Invalid ARQCInvalid card data
91Issuer unavailableService temporarily unavailable

Source: ISO 8583 / Mastercard Authorization Response Codes


8. Cardholder Data (CHD) and Sensitive Authentication Data (SAD)

For PCI DSS audit compliance - see Cardholder Environment for complete CDE overview.

8.1 Data in Transit (EMV Transactions)

Chip Contact & Contactless transactions transmit:

Data ElementClassificationTransmitted FromTransmitted ToEncryptionRetention
PANCHDCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transitEncrypted at rest (ChaCha20-Poly1305 via KMS)
Cardholder NameCHDCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transitEncrypted at rest (ChaCha20-Poly1305 via KMS)
Expiry DateCHDCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transitEncrypted at rest (ChaCha20-Poly1305 via KMS)
ARQCSADCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transit❌ Never stored (ephemeral, validated only)
ATCNon-sensitiveCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transitLast value stored (anti-replay tracking)
CVM ResultsNon-sensitiveCard chip → TerminalTerminal → Mastercard → Green-GotTLS 1.3 in transitTransaction log only
PIN BlockSADTerminal → Network(Online PIN only)TLS 1.3 in transit❌ Never stored (ephemeral)

Critical PCI DSS notes:

8.2 Data Storage (Green-Got Database)

What Green-Got stores (encrypted at rest):

Data ElementClassificationStorage LocationEncryption MethodRetention PolicyJustification
PANCHDDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetime + 7 yearsRequired for all transactions
Cardholder NameCHDDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetime + 7 yearsRequired for transactions
Expiry DateCHDDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetime + 7 yearsRequired for transactions
PVVSADDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetimeOnline PIN verification
Issuer-side encrypted PIN blockSADDatabase (encrypted)Inner PIN block retained under PEK_MC_AUTH, then encrypted at rest with ChaCha20-Poly1305 (KMS KEK)Card lifetimePIN reveal in dedicated iframe, Exceet transmission, EMV PIN change preparation
Green-Got online PIN counterSecurity stateDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetimeOnline PIN blocking independent from chip PTC
Mirrored chip PTCSecurity stateDatabase (encrypted)ChaCha20-Poly1305 (KMS KEK)Card lifetimeOffline PIN block status based on EMV chip data when available
Last ATCNon-sensitiveDatabase (plaintext)Not encryptedCard lifetimeAnti-replay protection

What Green-Got NEVER stores:

Data ElementClassificationPolicyReason
Track DataSAD❌ Never storedMagstripe supported but track data ephemeral only
ARQCSAD❌ Never storedEphemeral, validated during transaction only
ARPCSAD❌ Never storedGenerated per transaction, not persisted
Transport PIN BlockSAD❌ Never stored as a separate transport artifactGenerated on-demand for network transport or manufacturer communication

Issuer exception (PCI DSS):

8.3 Data Flow Entry and Exit Points

CHD Ingress (Entry to Green-Got):

  1. Card chip → Terminal → Mastercard → Green-Got
    • PAN, Cardholder Name, Expiry (every transaction)
    • Encrypted TLS 1.3 in transit

CHD Egress (Exit from Green-Got):

  1. Green-Got → Mastercard (authorization response)
    • No CHD in response (approval code only)
  2. Green-Got → Arkéa (clearing files)
    • PAN, transaction amount (encrypted files, 90-day retention)

SAD Flow:

8.4 Encryption Architecture (Reference)

For detailed encryption architecture, see Cryptography & Key Management.

Summary:


9. Used Components

9.1 Cardholder Data

9.2 Cryptographic Components

9.3 PIN Components

9.4 AWS Payment Cryptography

Green-Got uses AWS Payment Cryptography for all EMV cryptographic operations:


10. Related Documentation

10.1 Internal Documentation

10.2 External Standards

999. Uncertainties

This document reflects all the questions that still need to be solved and for which we wrote some documentation but without certainty.

Customers management

AML

Case management

Due diligence

Reporting

FICOBA

Tracfin

Account access

Account access: holders and participants

Two distinct concepts govern a bank account. Ownership answers “whose account and whose money is this?” — the holder. Access answers “who is allowed to do what on it?” — the participants. They are deliberately separate: a person can have access to an account they do not own (and an account is owned by exactly one holder, even when several people can act on it).

Holder — who owns the account

Every customer account has exactly one holder: the natural person or the legal entity that legally owns the account and the money in it.

The holder record (account_holder) is a thin link to the source-of-truth party table (physical_person or legal_entity, exactly one). The holder’s registered name — what Verification of Payee returns to a remitting bank — is read from that party record, never snapshotted. Internal/clearing/settlement accounts have no holder.

There is exactly one holder per account. “Shared account” does not mean several holders — it means one holder plus several participants (below).

Participant — who can act on the account

A participant is a natural person granted access to a holder’s accounts, with rights. A participant never owns the account or the money. Participants are always natural persons and are linked to a physical_person (and, once onboarded, to a login user).

Access is granted at holder level: a participant reaches all of the holder’s accounts by default, and that can be narrowed to specific accounts (per-account scope). Examples:

A person can be both an account participant and an organisation member — the two coexist. Account access is governed by participants; organisation membership is a separate, higher-level concept that (in future) creates participant grants for a company’s accounts.

Rights: roles and capabilities

A capability is a single right expressed as (permission, module) — for example Read · bank_account, Export · bank_account.as_accountant, Write · cards.virtual, Manage · bank_account.wire_transfers. Permissions and modules reuse the application’s permission catalogue.

A role is a reusable bundle of capabilities. System roles are shared across every holder; a holder may also define its own custom roles.

RoleWhat it can do (summary)
OwnerFull control: manage the account, transfers, cards and participants; export.
AdminDay-to-day management of transfers and cards; read/write/export; cannot transfer ownership.
ManagerA Member who can also see and validate the spending of the people who report to them.
MemberRead the account, manage their own card, initiate transfers (subject to approval).
AccountantRead and export transactions and accounting data only — no card, no transfer. Designed for an external accounting firm.
ViewerRead balance and transactions only.

A participant’s effective rights on an account are the role’s capabilities, plus any per-participant Grant overrides, minus any Revoke overrides — applied only to the accounts in the participant’s scope, and only while the participant is Active. This lets one person be granted (or denied) a specific capability without inventing a new role.

Hierarchy and validation

Participants can form a management hierarchy: a participant may report to a manager participant (within the same holder). This drives validation/approval — for example a manager who holds the team-spending capability can review and validate the spending of the participants who report to them. The hierarchy is a tree per holder; participants with no manager are top-level.

Card visibility exception

Cards are an exception to the holder/participant access rules. A card can be held by a participant, and only that participant may see the card’s sensitive details (PAN, CVC) — not even an Owner or Admin sees another participant’s card secrets. Managers and Owners may see a card’s existence and limits for oversight, but never its secrets. (See also “Customer sensitive actions” in the glossary.)

Named account parties (Verification of Payee)

Most participants only have access. A few are also a name the account can be paid under — a co-titulaire or a mandataire. These participants carry the appears_for_payee_verification flag.

This flag, and only this flag, controls what our managed VOP responder returns to GGBS when someone elsewhere pays one of our customers (see VOP). For a given IBAN we return:

Operational participants (accountant, viewer, employees) are never returned — they are not payees, and surfacing their name would let a transfer addressed to them match an account they merely operate. The flag defaults to off.

In practice:

This is how the “match one of the names on the bank details” rule for shared accounts (in transactions_sepa_beneficiaries_vop_internal.md) is satisfied: ownership stays with the single holder, while the named-party participants extend the set of names that legitimately match.

Documents management

Monitoring actions

Onboarding

Physical persons

KYC

Initial

Updates

Risk assessment

Initial

Updates

Screening

Initial

Updates

Profile creation

Database

Migrations

Create migrations

You can create a migration with gg db migrate add <migration_name> or gg data-warehouse migrate create <migration_name> Db migrations are generate 2 files a up.sql and down.sql in src/db/migrations. Data warehouse migrations generate only one .sql file in src/data_warehouse/migrations since it’s currently does not support reverable migrations duo to a limitation of the underlying library but that will be fixed with time.

Run migrations

Migrations are automatically run by gg deploy before the actual deployment but can also be run manually by calling gg {db, data-warehouse} migrate

Concurrent indexes and transaction breaks

Postgres CREATE INDEX CONCURRENTLY cannot run inside a transaction.

CREATE TABLE test_table (x int);
-- transaction-break
CREATE INDEX CONCURRENTLY test_table_x_idx ON test_table (x);
-- transaction-break
INSERT INTO test_table (x) VALUES (1);

Notes:

Reverse migration

Db migrations and hopefully soon also data warehouse migrations can be reverted with gg db revert

Automatic tests will ensure that a a down migration correctly reverts an up migration

Internals

There was a brief thought to use Barrel or SeaORM Migrations instead of raw SQL. This was aborted because of the lack of Clickhouse support and the inablity to do stored procedures.

Queries and Concurrency

Query types

We use different query types to handle concurrency in our database operations. The main types are:

Executor type

Depending on the type of query being executed, we use different executor types:

Each of these types help manage concurrency and provide intent through the function signatures to help enforce correct usage patterns.

When a store or use case function exposes a PgTransaction, it indicates that the query can only be executed within a transaction context.

Locking behavior

Some operations may require explicit locking to ensure data integrity during concurrent access. We use the following locking mechanisms:

The lock mechanisms are described in the store functions. It is an infrastructure responsibility.

Seeding

Now that an account does not need to exist on any third party provider we can provide proper seeding so data in every state is available for development or preview environments.

Seeding a database

To run the already existing seeds we just need to execute this command.

gg db seed <env>

Adding a seed

To add a seed we need to implement Seed on a struct. To ensure the most accurate data in the database the idea is to use the structs and method’s defined in ==models== and ==stores== to seed the database so we ensure we don’t just confirm to the database schema but also respect the application logic.

impl Seed for CreateOrganisation {
    const COUNT: u32 = 10;

    async fn fixture(self, pool: &db::Pool) -> Self::Result {
        create_organisation(pool, self).await?;

        Ok(())
    }
}

Then we just need to add the seed to the Seeds enum.

pub enum Seeds {
    Organisation(CreateOrganisation),
    InvoiceWithProducts(CreateInvoiceWithProducts),
}

Adding the capability to generate arbitrary data

Sadly this alone does not yet work. In addition we need to derive Dummy on the struct we implement Seed for and all child structs and enums. This macro allows us to generate arbitrary data for the whole struct. When we want or need to customize this generation process we can add individual annotations to fields to change the default generation like in this case we generate an email instead of a random string.

#[derive(Dummy)]
pub struct CreateOrganisation {
    pub vat_identification_number: Option<VatIdentificationNumber>,
    pub discount_conditions: Option<String>,
    pub late_payment_penalties: Option<String>,
    pub legal_fixed_compensation: Option<String>,
    pub invoice_identifier_prefix: Option<String>,
    pub quote_identifier_prefix: Option<String>,
    #[dummy(faker = "SafeEmail()")]
    pub email: String,
    pub transaction_type: Option<OrganisationTransactionType>,
    pub vat_payment_condition: Option<VatPaymentCondition>,
    pub capital_share: Option<i32>,
    pub res_number: Option<String>,
    pub logo_document_id: Option<i32>,
    pub invoice_additional_notes_fr: Option<String>,
    pub invoice_email_settings_subject_fr: Option<String>,
    pub invoice_email_settings_message_fr: Option<String>,
    pub quote_email_settings_subject_fr: Option<String>,
    pub quote_email_settings_message_fr: Option<String>,
}

Internals

How does this work without violating foreign key contraints?

It does not, we disable foreign key constraints so we can seed data in parallel. When using data models that reference other colums we just override the seed for the id column so generated data references entities we will generate.

Do seeds always generate the same data?

Generally yes, we hardcode the seed for the randomness we use to generate fake data and we generate it in deterministic order but adding or removing fields from existing structs will change the seeded data for that struct.

Retail Account Scenarios

Scenario-Based Retail Account Seeding

Summary

Retail account seed data should be modeled as complete user-facing scenarios, not as independent table seeds.

The scenario graph we want to seed is:

app_user
login_identifier
credential
bank_account
iban
retail_account
evented_ledger_bank_account_balance

The physical table name remains retail_account. The public/domain/API name remains Account.

The seeding pool already disables foreign key checks with SET session_replication_role = replica, so we can keep the speed advantage. The important constraint is that each seed scenario must generate all IDs for the graph up front, so references are coherent even when Postgres is not enforcing them during inserts.

Goals

Scenario Data

Seed 100 users so the account API can be tested against different authenticated users, account counts, and visibility states.

enum RetailUserState {
    Active,
    NegativeBalance,
    SuspendedBankAccount,
    BlockedBankAccount,
    ClosedBankAccount,
    NoIban,
    NoAccount,
}

struct RetailUserScenario {
    index: u32,
    state: RetailUserState,
    account_count: u32,
}

impl RetailUserScenario {
    fn all() -> Vec<Self> {
        (1..=100)
            .map(|index| Self {
                index,
                state: state_for_index(index),
                account_count: account_count_for_index(index),
            })
            .collect()
    }
}

This gives us:

Invalid records should live in tests instead of default seed data.

Seed Graph

Generate a complete graph before inserting anything. Keep user records and account records separate so a user can have zero, one, or many accounts.

struct SeedUserRecord {
    user_id: UserId,
    credential_id: CredentialId,
    login_identifier_id: LoginIdentifierId,
    email: String,
    password_hash: String,
}

struct SeedAccountRecord {
    user_id: UserId,
    bank_account_id: BankAccountId,
    account_id: AccountId,
    account_name: String,
    iban: Option<Iban>,
    primary_iban: bool,
    bank_account_status: BankAccountStatus,
    available_balance: i64,
}

The generated graph owns every ID used by every table. That lets us batch each table independently while still preserving coherent references.

Module Shape

Add a scenario seed. The implementation can live directly in the seeds crate because this scenario crosses authentication, user, core banking, IBAN, and retail account storage:

src/seeds/src/retail_account_scenarios.rs

Put the SQL batch helpers in a store module:

src/seeds/src/stores/retail_account/seed/*.rs

Keep non-seed retail account store code directly under src/seeds/src/stores/retail_account/*.

Expose:

#[derive(Dummy)]
pub struct CreateRetailAccountScenarios;

Register it in src/seeds/src/lib.rs:

use retail_account_scenarios::CreateRetailAccountScenarios;

mod retail_account_scenarios;
mod stores;

pub async fn seeds(pool: &db::Pool) -> eyre::Result<()> {
    kms::init_data_key_cache().await?;

    CreateOrganisation::seed(pool).await?;
    CreateInvoiceWithProducts::seed(pool).await?;
    Runner::seed(pool).await?;
    CreateTestUser::seed(pool).await?;
    CreateSettlementAccounts::seed(pool).await?;
    CreateIndividualCards::seed(pool).await?;
    CreateRetailAccountScenarios::seed(pool).await?;
    CreateBoOrganization::seed(pool).await?;

    Ok(())
}

Parallel Seed Flow

The seed should not create one scenario in a transaction. Instead:

  1. Build all scenario graphs in memory.
  2. Execute one Postgres batch statement per seed step.
  3. Run all batch statements concurrently.

Each function in the seed flow must be a batch operation over all rows:

seed_users(pool, &users)
seed_login_identifiers(pool, &users)
seed_credentials(pool, &users)
seed_bank_accounts(pool, &accounts)
seed_primary_ibans(pool, &accounts)
seed_retail_accounts(pool, &accounts)
seed_balances(pool, &accounts)

None of these functions should loop over rows and issue one query per row. The loop belongs only in memory when building vectors for UNNEST, JSON recordsets, or another Postgres-native batch input.

impl Seed for CreateRetailAccountScenarios {
    const COUNT: u32 = 1;

    async fn fixture(self, pool: &db::Pool) -> eyre::Result<()> {
        let scenarios = RetailUserScenario::all();

        let (users, accounts) = build_seed_graph(scenarios)?;

        tokio::try_join!(
            seed_users(pool, &users),
            seed_login_identifiers(pool, &users),
            seed_credentials(pool, &users),
            seed_bank_accounts(pool, &accounts),
            seed_primary_ibans(pool, &accounts),
            seed_retail_accounts(pool, &accounts),
            seed_balances(pool, &accounts),
        )?;

        Ok(())
    }
}

This gives us maximum useful parallelism:

The result is seven concurrent Postgres batch operations, not hundreds of small inserts.

Insert Strategy

Prefer batch inserts over one insert per row.

Prefer conflict-safe statements over exists checks:

ON CONFLICT (...) DO NOTHING

or, for deterministic fields that should be refreshed on rerun:

ON CONFLICT (...) DO UPDATE

This keeps seed reruns idempotent and avoids race windows between existence checks and inserts.

Every batch helper should follow this shape:

async fn seed_some_table(
    pool: &db::Pool,
    rows: &[SeedAccountRecord],
) -> eyre::Result<()> {
    let ids: Vec<_> = rows.iter().map(|row| row.some_id.as_str()).collect();
    let values: Vec<_> = rows.iter().map(|row| row.some_value.as_str()).collect();

    sqlx::query!(
        r#"
        INSERT INTO some_table (id, value)
        SELECT * FROM UNNEST($1::text[], $2::text[])
        ON CONFLICT (id) DO NOTHING
        "#,
        &ids,
        &values,
    )
    .execute(pool)
    .await?;

    Ok(())
}

Use UNNEST for simple column batches. If a table becomes awkward to express with parallel arrays, use a Postgres-native batch alternative such as jsonb_to_recordset, but keep it one SQL statement per helper.

Retail Account Insert

async fn seed_retail_accounts(
    pool: &db::Pool,
    rows: &[SeedAccountRecord],
) -> eyre::Result<()> {
    let account_ids: Vec<_> = rows.iter().map(|row| row.account_id.as_str()).collect();
    let user_ids: Vec<_> = rows.iter().map(|row| row.user_id.as_str()).collect();
    let bank_account_ids: Vec<_> = rows.iter().map(|row| row.bank_account_id.as_str()).collect();
    let names: Vec<_> = rows.iter().map(|row| row.account_name.as_str()).collect();

    sqlx::query!(
        r#"
        INSERT INTO retail_account (id, user_id, bank_account_id, name)
        SELECT * FROM UNNEST($1::text[], $2::text[], $3::text[], $4::text[])
        ON CONFLICT (id) DO NOTHING
        "#,
        &account_ids,
        &user_ids,
        &bank_account_ids,
        &names,
    )
    .execute(pool)
    .await?;

    Ok(())
}

Primary IBAN Insert

Use the primary column name, not is_primary.

async fn seed_primary_ibans(
    pool: &db::Pool,
    rows: &[SeedRetailAccountGraph],
) -> eyre::Result<()> {
    let ibans: Vec<_> = rows.iter().filter_map(|row| row.iban.as_ref()).collect();
    let bank_account_ids: Vec<_> = rows.iter().map(|row| row.bank_account_id.as_str()).collect();
    let primary: Vec<_> = rows.iter().filter(|row| row.iban.is_some()).map(|row| row.primary_iban).collect();

    sqlx::query!(
        r#"
        INSERT INTO iban (iban, bank_account_id, "primary")
        SELECT * FROM UNNEST($1::text[], $2::text[], $3::bool[])
        ON CONFLICT (iban) DO NOTHING
        "#,
        &ibans,
        &bank_account_ids,
        &primary,
    )
    .execute(pool)
    .await?;

    Ok(())
}

Balance Update

Seed available_balance as an i64. Negative balances are valid seed data.

The account API should return a frontend amount shape with an exponent, but the internal Amount type should stay internal. The exponent should be derived from the currency.

async fn seed_balances(
    pool: &db::Pool,
    rows: &[SeedAccountRecord],
) -> eyre::Result<()> {
    let bank_account_ids: Vec<_> = rows.iter().map(|row| row.bank_account_id.as_str()).collect();
    let balances: Vec<_> = rows.iter().map(|row| row.available_balance).collect();

    sqlx::query!(
        r#"
        UPDATE evented_ledger_bank_account_balance balance
           SET cleared_credit = data.available_balance,
               cleared_debit = 0,
               authorized_debit = 0,
               updated_at = NOW()
          FROM UNNEST($1::text[], $2::bigint[]) AS data(bank_account_id, available_balance)
         WHERE balance.bank_account_id = data.bank_account_id
        "#,
        &bank_account_ids,
        &balances,
    )
    .execute(pool)
    .await?;

    Ok(())
}

Bank Account Insert

The existing create_bank_account helper inserts both bank_account and evented_ledger_bank_account_balance.

For this scenario seed, add a batch seed-only helper instead of calling create_bank_account per row.

To preserve maximum parallelism, seed_bank_accounts should insert only bank_account rows. seed_balances should independently insert/update evented_ledger_bank_account_balance rows. FK checks are disabled by the seeding pool, so both helpers can run at the same time.

async fn seed_bank_accounts(
    pool: &db::Pool,
    rows: &[SeedAccountRecord],
) -> eyre::Result<()> {
    let bank_account_ids: Vec<_> = rows.iter().map(|row| row.bank_account_id.as_str()).collect();
    sqlx::query!(
        r#"
        INSERT INTO bank_account (id, country_code, status)
        SELECT bank_accounts.id, 'Fr'::countrycode, 'Active'::BankAccountStatus
        FROM UNNEST($1::text[]) AS bank_accounts(id)
        ON CONFLICT (id) DO UPDATE
            SET country_code = EXCLUDED.country_code,
                status = EXCLUDED.status,
                deleted_at = NULL,
                updated_at = NOW()
        "#,
        &bank_account_ids,
    )
    .execute(pool)
    .await?;

    Ok(())
}

The exact enum/type casts should match the real database type names. The important design rule is that bank account creation is still one Postgres batch operation, and balance creation is a separate Postgres batch operation that runs concurrently.

User, Login Identifier, Credential Inserts

Use the same authentication concepts as CreateTestUser, but batch them for the scenario rows.

The password hash can be computed once for password123 and reused across rows.

The seed should create:

Each of these must be its own batch helper and each helper should issue one Postgres batch statement.

Use conflict-safe inserts keyed by stable identifiers where possible, especially email/login identifier keys.

The user/auth batch helpers should be run in the same top-level tokio::try_join! as the account helpers. They should not be sequenced before the account helpers, because FK checks are disabled in the seed pool and the complete graph already owns coherent IDs.

API Expectations

Do not change the account API behavior as part of this seed.

list_account and get_account should continue to return only:

The API should return balance, where that value is the available balance.

The API should not expose bank account status for now.

Migration And Model Notes

Test Plan

Implementation is separate from tests, but the eventual verification should cover:

  1. Run cargo check.
  2. Run the local database reset/seed flow.
  3. Find retail.account@green-got.com.
  4. Call list_account with that user’s mock-auth context.
  5. Verify exactly the expected account is returned.
  6. Call get_account for that account ID.
  7. Verify the returned account matches the list result.
  8. Verify the returned balance shape:
{
  "value": 14940,
  "currency": "EUR",
  "exponent": 2
}
  1. Run seeds twice and confirm the result remains idempotent.

Open Implementation Decisions

Domain-Driven Design

The core repository follows Domain-Driven Design (DDD), a methodology for “Tackling Complexity in the Heart of Software.” This approach originates from Eric Evans’ book, commonly known as the Blue Book.

DDD helps design software that accurately represents business domains using a shared language, known as the ubiquitous language. This common language improves communication between developers and domain experts.

As you learn more about the business, your understanding evolves. You’ll often need to refactor code to reflect new rules, updated understanding, or changes in the underlying technology. DDD supports this process by isolating the domain and organizing your software into clear layers, making the system easier to maintain and adapt over time.

Domain-Driven Design and Hexagonal Architecture share the same core principle of isolation, making them complementary. While DDD emphasizes deeply understanding and modeling the business domain, Hexagonal Architecture focuses on cleanly separating that domain from external concerns through ports and adapters. You can think of Hexagonal Architecture as a structural layer that reinforces DDD’s boundaries, ensuring the domain logic remains independent from infrastructure and interface details. Together, they promote maintainable, testable, and well-structured code.

Bounded Context

As a project grows, so do the different contexts in which you apply your business rules. A User in one context will have a different definition than in another. By explicitly setting boundaries, we keep our models consistent within those limits and avoid overlapping information processing, which could negatively impact our business rules and invariants (e.g., a user must always have a unique email).

For example, in the context of Authentication, the aggregate UserAuth may contain credentials, session data, and roles. Once the user is authenticated, session-related data becomes a technical detail handled by the infrastructure layer.

Now consider another context: Invoices. Here, your user representation will go through a different aggregate, UserInvoices, which includes the user, their invoices, and other data related to this specific context. The business rules are different, now the user can list their invoices, create new ones, manage drafts, and so on. You’re operating in a different context or zone of your application.

Have you ever encountered a case where you had one giant model/entity trying to manage all the data from different contexts, filled with conditional logic to include or exclude information based on the current processing needs? If so, you can see how boundaries and contexts help bring clarity and maintainability.

In DDD, bounded contexts are intentionally isolated, each with its own model, rules, and language. However, they often still need to interact. This communication typically happens through well-defined contracts, such as APIs, domain events, or anti-corruption layers. These mechanisms help translate or adapt data between contexts while preserving the internal consistency of each context’s model.

One concrete bounded context that you can find in our codebase is core_banking.

Code organization

When working with Domain-Driven Design, it helps to structure your code in a way that makes your intentions clear. The goal is not to over-engineer things from day one, but to keep your codebase clean and maintainable as it grows.

Structuring your code

A common approach is to separate your code into layers, each with a clear responsibility:

You don’t have to create every layer on day one. Start simple, and let the structure grow as your needs evolve. Each of these layers should live in their respective crate or module if you are in a bounded context. A good example of organization can be found in our core_banking bounded context.

Domain building blocks

To help you stay consistent when writing domain code here are the main building blocks:

Try to keep domain logic inside these domain types, and avoid leaking business rules into infrastructure or service code.

Code organization example

Here’s a simple example of how you might organize a crate:

core_banking/
├── src/
│   ├── application/
│   │  ├── use_cases/
│   │  │   ├── get_user_invoices.rs
│   │  │   ├── list_all_invoices.rs
│   │  │   ├── search_invoices.rs
│   ├── domain/
│   │  ├── user.rs
│   │  ├── invoice.rs
│   │  ├── services/
│   │  │   ├── invoice_service.rs
│   ├── stores/
│   │  ├── user_store.rs
│   │  ├── invoice_store.rs
│   ├── presentation/
│   │  ├── http_api.rs
│   │  ├── cli.rs
│   ├── docs/

Resources

You can learn more about DDD from the resources we shared in Notion

Financials

Accounts chart

Green-Got accounts

The accounts we have with Arkea

Operations

The money there elongs to Green-Got.

Segregation

Where our customers money should be 99.9% of the time.

Settlement

Buffer account to receive and send money.

Ledger

Fraud

Forensic

Monitoring

Prevention

Reporting

Getting started

Tips

A collection of tips when you work on this project.

Easy macro dev

Install the cargo expand module with cargo install cargo-expand.

In dev_utils::macros, create my_module

mod my_module {
    #[macro_export]
    macro_rules! test_macro {
        ($t:ident) => {
            pub struct $t {
                field1: String,
                }
            }
        }
    test_macro!(MyStruct);
}

Then, in the root of the project, run : cargo watch -q -c -x 'expand dev_utils::macros::my_module'

You should see the expanded code in the terminal :

mod my_module {
    pub struct MyStruct {
        field1: String,
    }
}

Repository architecture

From root

Database

For the database, we use Postgres 17. The server is running on the 5432 port through the Docker Compose. The user is postgres. There is no password needed. You will still see a secret defined in development.rs but this is simply for consistency with staging and production

[!CAUTION] This will delete the database if it exists and create it again ; then apply the migrations.

Documentation

2. Markdown Front Matter headers

Crabodex needs a Front Matter header in your markdown files. It only uses the ones that have it.

Here is an example of a markdown file with a Front Matter header:

---
position: 2
path:
  - Usage
  - CLI
  - Markdown Front Matter headers
vanta:
  url: https://app.eu.vanta.com/c/green-got.com/tests/example-test
  status: Submitted
---

The Front Matter header is a YAML block that starts and ends with three dashes. It contains key-value pairs that Crabodex uses to build the documentation:

Notes:

1. Local development review

You can generate and review your local documentation by visiting http://localhost:8080/development/documentation after starting your local development environment with gg dev.

Environnments

There are multiples environments available. They all have their own docker-compose file.

Dev

The dev environment is the one you should use for development. It should be running on your machine while coding. It is started for you by cargo setup (via docker compose up --detach); to start or restart it manually run docker compose up --detach from the docker_compose/ directory.

This environment starts PostgreSQL, ClickHouse, Temporal, and Mongo.

Staging

The staging environment is the one used for testing. You can access the logs in Grafana.

Get your rust environment ready

  1. Install Tailscale

  2. Install Docker (Docker Desktop or OrbStack for example)

  3. Install Rust: curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly -y

  1. Install CLI: cargo setup

Optional

Install Bun: curl -fsSL https://bun.sh/install | bash

Install biome: cd src/infrastructure && bun add --dev --exact @biomejs/biome

Mac only

For macos you need to adjust the max amount of file locks to avoid ProcessFdQuotaExceeded

https://github.com/rust-cross/cargo-zigbuild/issues/329

  1. Run code /Library/LaunchDaemons/limit.maxfiles.plist

  2. Update file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>limit.maxfiles</string>
        <key>ProgramArguments</key>
        <array>
            <string>launchctl</string>
            <string>limit</string>
            <string>maxfiles</string>
            <string>unlimited</string>
            <string>unlimited</string>
        </array>
        <key>RunAtLoad</key>
        <true />
        <key>ServiceIPC</key>
        <false />
    </dict>
</plist>
  1. Save as sudo

  2. Reboot

  3. Run launchctl limit maxfiles to verify

This is the ONLY way to do it. Things like using ulimit does not work and the output will actively lie to you. The max limits can only be changed this way

Request anatomy

An end-client wants to get a transaction details

  1. The client sends a request to the retail API from his mobile device.
  2. main – The server started in the main function receives the request and routes it to the retail API (and inject the transaction store and other data providers required). can throw an error (404)
  3. logging module – From now, the logging module will log everything that happens to the request.
  4. context module – The context middleware of the retail API adds a context to the request (request_id, user_id, etc.).
  5. retail API – The retail API middleware manage the authentication and authorization of the request and adds the result to the context. can throw an error (401, 403)
  6. retail API – The retail API receives the request and routes it to the transaction details endpoint. can throw an error (404)
  7. retail API – The transaction detail endpoint calls the useCase method in the retail API (in application) module.
  8. retail API – The useCase method calls the rules to check if the user is allowed to see the transaction details. It injects the transaction store (in store) module.
  9. domain – The rules are checked to allow the transaction details to be seen by the user. can throw an error (401, 403, 500)
  10. domain – The useCase method calls the getTransactionDetails method in the transaction store (in the store that was injected in the API by the main) module.
  11. store – The transaction store calls the getTransactionDetails method in the database (in database) module. can throw an error (404)
  12. Result is returned to the end client.

MasterCard wants to know if we authorize a transaction

  1. MasterCard sends a request to the MasterCard WebHook from their servers.
  2. main – The server started in the main function receives the request and routes it to the MasterCard WebHook (and inject the transaction store and other data providers required). can throw an error (404)
  3. logging module – From now, the logging module will log everything that happens to the request.
  4. context module – The context middleware adds a context to the request (request_id, user_id, etc.).
  5. mastercard webhook – The MasterCard authentication middleware manage the authentication and authorization of the request and adds the result to the context. can throw an error (401, 403)
  6. mastercard webhook – The MasterCard WebHook receives the request and routes it to the transaction authorization endpoint. can throw an error (404)
  7. mastercard webhook – The transaction detail endpoint calls the useCase method in the retail API (in application) module. It calls now the integration of the ISO 8583.
  8. integration – The 8583 module translates the message into an object corresponding to our internal model and sends back to the useCase method.
  9. mastercard webhook – The useCase method calls the rules to check if the user is allowed to see the transaction details. It injects the transaction store (in store) module.
  10. domain – The rules are checked to authorize or decline the transaction.
  11. store – The required stores are called to gather the necessary information to authorize or decline the transaction.
  12. domain – The domain authorize or decline the transaction calls the store for the synchronous updates and send the response to the usecase method. can throw an error (401, 403, 500)
  13. store – The balances and last transaction request date are updated in the account store.
  14. mastercard webhook – The usecase calls the integration to translate the response into an ISO 8583 message.
  15. integration – The 8583 module translates the message into an ISO 8583 message.
  16. mastercard webhook – The response is sent back to MasterCard.

Asynchronous from 12.

  1. domain – The domain starts a workflow in temporal to create the transaction.
  2. store – The ledger is updated with the new transaction.
  3. integration – The transaction is forwarded to HawkAi
  4. domain – The transaction enrichment module is called to enrich the transaction.
  5. store – The transaction is updated with the new information.

An employee validates a new client account creation

  1. The employee clicks the button to validate the account creation and the back office send a request to the private API.
  2. main – The server started in t+he main function receives the request and routes it to the private API (and inject the transaction store and other data providers required). can throw an error (404)
  3. logging module – From now, the logging module will log everything that happens to the request.
  4. context module – The context middleware adds a context to the request (request_id, user_id, etc.).
  5. private API – The private API middleware manage the authentication and authorization of the request and adds the result to the context. can throw an error (401, 403)
  6. private API – The private API receives the request and routes it to the validate account creation endpoint. can throw an error (404)
  7. private API – The validate account creation endpoint calls the useCase method in the private API (in application) module.
  8. private API – The useCase method calls the rules to check if the user is allowed to validate the account creation. It injects the account store (in store) module.
  9. domain – The rules are checked to allow the account creation to be validated. can throw an error (401, 403, 500)
  10. domain – The store are called for synchronous updates ; then the result is sent to the useCase method.
  11. private API – The useCase method returns the result to the end client.

Asynchronous from 5.

  1. private API – The api calls the domain to feed the audit trail.
  2. domain – The domain starts a workflow to feed the audit trail.
  3. store – The audit trail is updated with the new information.

Asynchronous from 10.

  1. domain – The domain starts a workflow in temporal to create the account and to start the integrations.
  2. store – The account is created in the database.
  3. integration – Ficoba report is build and sent.

Run dev environment

The dev services — Postgres, ClickHouse, Temporal and Mongo — are defined in docker_compose/docker-compose.yaml and are started for you as part of cargo setup (which runs docker compose up --detach).

To start or restart them manually, run docker compose up --detach from the docker_compose/ directory. See Environments to learn more about the various environments.

Start the server

For development, start the server with: gg d.

Tests

(You can find the aliases in the .cargo/config.toml file)

Just like in the Typescript backend, you can run tests in a specific crate with cargo <crate_name> test.

[!TIP] All the command aliases are in the .cargo/config.toml file.

End to end tests

We have some end to end tests that are behind a feature flag. This feature flag is e2eand is disabled by default.

Update base docker image

To update the base docker image run

docker build -t ghcr.io/green-got/core:latest docker push ghcr.io/green-got/core:latest

Glossary

This glossary serves as a unified reference for all teams involved in Green-Got. Its primary goals are to:

This glossary contains:

Using this glossary will help maintain consistency, improve clarity, and reduce misunderstandings throughout the development and operation of our banking application.

A

ABS: Arkea Banking Services. They are our banking partner technical entity.

Account holder: The single natural person or legal entity that legally owns an account and the money in it. Exactly one per account (a personal account is held by a physical person; a business account by the company). Distinct from an Account participant, who only has access. See Account access.

Account participant: A natural person granted access to a holder’s account(s) with specific rights (a Role plus optional per-participant overrides). A participant does not own the account or the money. Used both for shared personal accounts and for business-account teams (CEO, manager, employee, accountant). See Account access.

ACH: Automated Clearing House. In the US, they are the settlement banks that all the other banks are connected to in order to send money to each other.

ACPR: Autorité de Contrôle Prudentiel et de Résolution. French prudential supervision and resolution authority, regulator for the PSPs

Acquiring: Bank: Bank that processes payments for merchants

AML: Anti-Money Laundering. Procedures to prevent money laundering

API: Application Programming Interface. Protocol for software communication

Authentication: The process of verifying the identity of a physical person using one of our applications.

AWS: Amazon Web Services. Amazon’s cloud computing platform offering a wide range of services including computing, storage, databases, and networking capabilities.

B

Balance: The current amount of money in an account. It is always positive (credit balance). It can appear negative (debit balance or overdraft), but this is a way for us to tell the account holder that they own us the money of the subscription.

Bank Details: A document with the base informations about the account. (RIB in french)

Bank Statement: A periodic record of account activity and balances.

BIC: Bank Identifier Code. International bank identification code. This is issued by SWIFT.

BIN: Bank Identification Number. First digits of a payment card number

C

Capability: A single right on an account, expressed as a (permission, module) pair (e.g. Read · bank_account, Export · bank_account.as_accountant). Roles bundle capabilities; an Account participant may also carry per-participant grant/revoke overrides.

CD: Continuous Delivery/Deployment. An automated software release process that enables frequent, reliable software deployments either with manual approval (Delivery) or automatically (Deployment).

CDD: Customer Due Diligence. Process of verifying customer identity

CI: Continuous Integration. An automated software development practice where code changes are regularly built, tested, and merged to a shared repository.

Client: Forbidden word. It can have a technical (application installed on device) and a business meaning.

Cloud: A technology that delivers computing services (servers, storage, databases, networking, software) over the internet instead of local hardware.

CNIL: Commission Nationale de l’Informatique et des Libertés. French data protection authority

CSM: Clearing and Settlement Mechanisms. Processes and systems that enable the transfer of financial assets. Also said settlement banks (see ACH).

Customer sensitive actions: Customer actions related to payment:

D

DSP/PSD: Payment Services Directive. EU directive regulating payment services

DD: Direct Debit. Automated payment collection from bank account (see SDD in our case)

DDD: Domain-Driven Design. A software design approach. The term was coined by Eric Evans in his book.

E

EBA: European Banking Authority. EU banking regulatory agency

EEA: European Economic Area. EU plus Iceland, Liechtenstein, and Norway

EMV: Europay, Mastercard, Visa. Technical standard for smart cards

EP: Établissement de Paiement. Payment Institution

F

FATF: Financial Action Task Force. International AML/CFT standard setter

G

Git: A distributed version control system that tracks changes in source code during software development.

GitHub: A web-based platform for hosting Git repositories that provides collaboration features, version control, and code management tools.

H

I

IBAN: International Bank Account Number. Standardized bank account number

IP: Institution de Paiement. Payment Institution (French)

ISSP: Information Systems Security Policy. IT security framework

J

K

KYB: Know Your Business. Business customer due diligence

KYC: Know Your Customer. Customer identification and verification

KYT: Know Your Transaction. Transaction monitoring and analysis

L

LCB-FT: Lutte Contre le Blanchiment et le Financement du Terrorisme. French AML/CFT

M

MAD: Market Abuse Directive. EU directive on market abuse

MCC: Merchant Category Code. Four-digit code classifying merchants

MIF: Multilateral Interchange Fee. Card transaction fee between banks

N

Named account party: An Account participant who is also a name the account can be paid under — a co-titulaire or mandataire — flagged appears_for_payee_verification. Only named parties (plus the holder) are returned by our managed VOP responder; operational participants are not. See Account access.

O

P

PAN: Primary Account Number. Payment card number

PCI-DSS: Payment Card Industry Data Security Standard. Card security standard

PEP: Politically Exposed Person. High-risk customer category

PIN: Personal Identification Number. Secret code for card authentication

PSP: Payment Service Provider. Entity providing payment services

PSEE: Prestataires de Services Essentiels Externalisés. Critical external service providers

PUPA: Plan d’Urgence et de Poursuite d’Activité. Business continuity plan

Q

R

RTS: Regulatory Technical Standards. Detailed regulatory requirements

RACI: Responsible, Accountable, Consulted, Informed. Responsibility assignment matrix

Role: A reusable bundle of capabilities assigned to an Account participant (e.g. Owner, Admin, Manager, Member, Accountant, Viewer). System roles are shared across holders; a holder may define its own custom roles. See Account access.

S

SCA: Strong Customer Authentication (SCA). Requirements to enhance security for electronic payments.

SCT: SEPA Credit Transfer. European credit transfer scheme

SDD: SEPA Direct Debit. European direct debit scheme

SEPA: Single Euro Payments Area. European payment integration initiative

STET: Systèmes Technologiques d’Échange et de Traitement. French CSM

SWIFT: Society for Worldwide Interbank Financial Telecommunication. Global financial messaging

T

TARGET: Trans-European Automated Real-time Gross Settlement Express Transfer. EU payment system

TIPS: TARGET Instant Payment Settlement. European instant payment system

TPP: Third Party Provider. External payment service provider

TRACFIN: Traitement du Renseignement et Action contre les Circuits Financiers clandestins. French FIU

U

User: Almost forbidden word. Always refer to the person using the application by their role in Green-Got. Account holder, Compliance officer, employee

UBO: Ultimate Beneficial Owner. Natural person ultimately owning/controlling entity

V

VOP: Verification of Payee. The payee-name check run before a SEPA credit transfer (does the entered name match the destination IBAN’s holder?). We use GGBS as our provider, both as requester (we ask) and in managed mode (GGBS asks us). See VOP.

VPC: Virtual Private Cloud. An isolated, private section of a cloud platform where you can deploy your resources in a controlled, secure environment.

VPN: Virtual Private Network. A secure, encrypted network connection that enables private communications over a public network like the internet.

W

X

Y

Z

Infrastructure

Antivirus

@fabien-h

I have no idea here. For me it does not really make sense. I would rather setup something on the codebase, container registry level.

Backup

We store our data in several different locations. We will outline here the backup strategy for each location.

Database

We run 3 database instances of AWS Aurora cross 3 AWS Availibity Zones. AWS Aurora automatically divides our database volume into 10 GB segments spread across many disks. Each 10 GB chunk of our database volume is replicated six ways, across three AZs. Amazon Aurora is designed to transparently handle the loss of up to two copies of data without affecting database write availability and up to three copies without affecting read availability. AWS Aurora storage is also self-healing. Data blocks and disks are continuously scanned for errors and repaired automatically so there is no data loss.

If data loss should occur because of an application error on our side we have the ability to use our 30 days of continuous backups to restore the state of the database to any point on time within 1 second of precision. The latest restorable time will never be more than 5 minutes from the current time.

In addtion to this we will also keep daily snapshots of our database that can be kept indefinite.

In memory

If data does not require a 100% persistance garantee we might buffer it in memory for a period of time. For this duration in time it can come to data loss. We only use this mechanism of the data is of low importance like logs, analytics, etc.

Data warehouse

For data that is meant for analytics or infrequent access we use Clickhouse Cloud. None of the data stored here is required for the operation of Green-Got and data loss or unavilablity would not impact core functionality for customers.

The data storage of Clickhouse Cloud is build on S3 which offers a 99.999999999% durability SLA The data warehouse is backed up every day and backups are retained for 2 days. We can increase the backup frequency and storage duration to every 6 hours and stored for 30 days.

Object Storage

We use object storage to store documents like a customers id document, pdfs, images, etc. While our application never deletes documents except for compliance purposes we still have Object versioning enabled so that an application error cannot delete a file without us keeping a backup of it. This backup time is for now set to 30 days but is configurable. S3 also has the option to enable backup S3 Backups but because of the high durablity of the service with 99.999999999% we feel its not nessesary for our usecase.

Durable execution

We use Temporal which is an open source project for durable execution. Data loss for active workflows is critical since it’s needed to drive a workflow to completion. TODOwaiting on reply about this. Closed workflow’s history is currently kept for 30 days and can be extended to 90 days with the option to also backup to S3. Since data for closed workflows can be considered obvervability data we decided its not needed to create a backup of it.

Obvervability

For obvervablity purposes we use Grafana Cloud. Logs and traces are retained for 30 days and metrics for up to 13 months. There are no backup strategies that have been communicated to us but because of the nature of the data loss is acceptable.

DDoS

To prevent Green-Got’s infrastructure again DDoS attacks we take the following measures.

Minimize attack surface

All our servers have a security group defined AWS EC2 Security Group. We use 3 different but very simple security groups.

Instance Security Group

Database security Group

Load balancer security Group

With this setup we delegate most of the DDoS protection up to Layer 6 to AWS Shield

Layer 7 DDoS protection

  1. Limit the ASN based on the CloudFront-Viewer-ASN header
  2. Limit server to server connections to using an IP allowlist
  3. Block certain geolocations
  4. Block “bad ips”
  5. IP based rate limit
  6. User based rate limit
  7. Overprovision

INTERNAL For my perspective AWS Shield Advanced (3k a month) AWS WAF are too expensive. We could do a setup where we can enable the WAF selectively. Also on Cloudflare sadly good Layer 7 DDoS protection is a feature of Enterprise

Logging

Since we’re in an asynchronous environment, we need to use traces instead of raw log lines. We use the tracing crate to do so.

I strongly recommend you to read the tracing documentation to learn more about how it works.

How to use it

Middleware

For the requests to be traced, we need to add the tracing middleware to the desired router.

let name: String = "Retail API".to_string();

// Here we create the router to handle the business logic
let retail_router = Router::new()
    .route("/", get(controllers::retail_ctrl::get_retail))
    .route(
        "/get-own-transaction/{id}",
        get(controllers::retail_ctrl::get_own_transaction),
    );

// Here we add the tracing middleware to the router
let retail_router = add_tracing_middleware(retail_router).layer(Extension(Context::new(&name)));

Notice that we also add the context middleware BEFORE the tracing middleware. This is because the tracing middleware needs the context to be able to trace the requests, since we add data to the traces that commes from the context (Request ID for example).

Instrumenting

Instrumenting is the process of adding a span to a function. To instrument a function, we need to add the tracing::instrument attribute to it. This will create a span for the function and add it to the trace.

#[instrument]
fn get_data() {
    // Do something
}

This should be added to almost every functions, since it’s how we can have a detail backtrace.

Take the time to read the tracing documentation to learn more about how to use it. It’s an important tool that you will use a lot.

Logging

You can still log things the old way, using the info!, warn!, error! and debug! macros.

#[instrument]
fn get_data() {
    // Do something
    info!("This is a log");
}

This allows you to log custom info to the trace. Note that the log will be added to the current span, so it’ll be easy to find it in the trace.

Configuration

The global subsriber configuration is written in the tracing_config file. This file describes, how the traces are formatted, wich data is included and where the traces are written to. It also defines the default log level.

See Environments to see where the traces are written to in each environment.

Open banking

Authentication

Assumptions

To write about this it makes sense to make some assumptions about our setup. The assumptions are the following.

Api key

We want to be heavily inspired by https://docs.qonto.com/api-reference/business-api/authentication/api-key.

Use case

API keys can be used to authenticate requests against the open banking API.

Technical details

If it’s an business API keys belong to the organisation. If it’s an individual they belong to the user. As of now API keys don’t expire. Also in Quonto you can only have one API key and it has a fixed set of permissions. I would change this to allow people to have multiple API keys (but limited to a reasonable number) and customize the permissions. The case for multiple API keys, is that in case your want to rotate an API key the Quonto system of having one does not allow this without downtime. The case for customizing permissons is to allow people to apply the principle of least permisson.

I would implement API keys as a separate table linked to a user or organization. With roughly this schema.

struct ApiKey {
    id: i32,
    user_id: Option<i32>,
    organisation_id: Option<i32>,
    secret: Secret,
    created_at: DateTime,
    deleted_at: Option<DateTime>,
    //Structure more explored later on
    permissons: Permissions
}

Since this lookup will be very highly frequented and this table an very read heavy I would keep a local cache around of all API keys. Even for a million API keys we should not need more than 100MB of memory to keep track of them but we would have the benefit of avoiding the network latency and the additional load on the db. It should be sufficient to just ask every second for the latest deleted API keys and latest added API keys using polling to keep the local version up to date. This would also perfectly fit distributed SQLite like https://fly.io/docs/litefs or https://turso.tech but it’s the question if it’s really needed for us to add something additional like this for this usecase. Interesting article related to this https://fly.io/blog/operationalizing-macaroons.

Ops

Business continuity

Disaster recovery

Compliance

Continuous improvement

Environments

We have a 4 tiered deployment structure. It allows for a systematic progression of code and applications from development to production, with increasing levels of security and scrutiny at each stage. It helps ensure that by the time an application reaches the production environment, it has been thoroughly tested for both functionality and security.

Development enviroment

This is a local enviroment that lives on the developer machine. They can write, test and debug code there.

Characteristics :

Preview enviroments

They are a short lived version of the staging environment. The goal is to expose a version in progress of the developper work.

Characteristics :

Staging enviroment

Hosts a live version of the application dedicated to internal testing. It’s supposed to be as close as possible to the production environment in terms of functionnalities but with fake data.

Characteristics :

Production enviroment

Hosts the live application. Processes actual perosnal data and payment data includign card data.

Characteristics :

General considerations :

Keys

We implement a robust key management system to ensure compliance with security standards.

Master Key

Key Generation and Distribution

  1. It starts with a member of the team that has a secret or a key that he wants to add or to rotate
  2. They use an internal command-line tool to send this secret and get an encrypted version of the key. The request is sent via encrypted traffic to our key generation service.
  3. The service returns an encrypted version of the key.
  4. They add the encrypted key to the code repository as an environment variable in a file with all the encrypted secrets

Storing encrypted keys in the code repository is safe because:

Key Deployment and Usage

  1. During server deployment, the master key is retrieved from AWS KMS.
  2. All secrets (encrypted keys) are decrypted using the master key.
  3. Decrypted keys are stored in server memory for use.
  4. Master key is discarded

Encryption Algorithm

We use ChaCha20-Poly1305 for encryption, which is:

Crypto Period and Master Key Rotation

Third party and internal secrets rotation

Benefits

Potential issues and our solutions

Single point of failure with the master key. => In the unlikely event where the master key is lost, the servers keep running with their current keys. We cannot deploy new instances because the deployment would fail. We have to generate a new master key, ask for new secrets to our providers and regenerate encrypted secrets for the environment file.

Risk of keys being exposed if server memory is compromised. => This is not related to the way we manage our key. Standard protections applies.

Lack of audit trail for key usage. => Adding or updating a key is visible in the version control system history (git). Decryption happens by default at every deployment and does not require monitoring.

Potential for unauthorized key generation if the internal command-line tool is compromised. => Calls to the encryption service are monitored and under alert. Every call can only be performed behind tilscale, our VPN. It requires a two factors authentication.

Monitoring

Process monitoring

Sub process monitoring

Monolith deployment process

The base is n (10?) containers idempotent behind a load balancer that in behind cloudfront to expose it the network.

Additionnaly, we have m (?) container dedicated to self starting workflows.

TODO

Decide what processes we put where.

Providers

Providers list :

Vendor assessement and selection :

TODO

Contractual requirements list :

TODO

(PSEE regulation)

When and how do we re-evaluate :

TODO

Roles

This breakdown provides a general scope for each role or team. In practice, there may be some overlap or additional responsibilities depending on the specific organization’s structure and needs. One person can be in multiple team and have cumulative accesses.

Executive role (CISO / CTO)

Access

IT Security team

Access

Development team

Access

Operations team

Access

Compliance Officer

Access

Customer Care Team

Access

Security

Technology stack

Incoming request from clients and providers goes through:

Other integrations

Annuaire entreprises

Arkineo

Checkout

Efficiale

Intercom

Ubble

Yousign

Products

Card insurances

We partner with Owen https://www.get-owen.com/ to add insurances to our debit cards. The insurance is attached to a cardholder and an account.

Most product accounts come with a default insurance contract. Insurances are linked to an account and a physical person or a business.

Rules

Insurance ID format

This is to avoid collision that could occur if a customer subscribe to a product and upgrade the same day (downgrade does not cause any issue)

If a product account has an optional physical card:

The definition of the insurance packages is done offline with Owen. We have the contract types from their API.

When a customer subscribes to an insurance, they get a link by email to open their account with owen.

Claims are filed in owen system. If a customer needs to use the insurance, the customer care have to redirect them to their personal space inside Owen.

The application of insurances on virtual cards and one off cards are in discussion with Owen.

We are the one managing which card is insured. The insurance is not linked to a card Owen side.

Owen technical integration

Their documentation is here: https://documentation.get-owen.com/ (but it’s not up to date)

To access the API, you need a x-api-key and an Authorization Bearer token. To get the token you need a login and a password. Ask in the team to get access to the sandbox.

What you need in you environment:

login : {{login}} password : {{password}} x-api-key : {{key}} host : {{host}}

The host for the sandbox is https://apidevelopers.get-owen.com.

To get the token, you have to run:

curl --location -g '{{host}}/auth/partners/signin' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --data-urlencode 'email={{login}}' \
  --data-urlencode 'password={{password}}'

That should return your {{token}}

{
  "success": true,
  "status": 200,
  "response": {
    "email": "{{login}}",
    "accessToken":"{{token}}"
  }
}

You can then use it to query the API. It’s valid for 24 hours. But let’s refresh it more often.

Close adhesion

It’s a simple PATCH request:

curl --location -g --request PATCH '{{host}}/adhesions/{{adhesion_id}}/endofcontract' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --form 'action=cancel' \
  --form 'cancelationType=resiliation' \
  --form 'cancelationDate=2024-09-30 05:09:20.043Z'

TODO

Create adhesion

It’s a simple POST request:

curl --location '{{host}}/owen-products/adhesions' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --header 'Authorization: Bearer {{token}}' \
  --data-urlencode 'contractType=66ec3637839b0ba2569a3d18' \
  --data-urlencode 'channel=666b030857465219254fdfad' \
  --data-urlencode 'contractPeriod=640b3f21f9c3ad4c73ee6d24' \
  --data-urlencode 'contractPeriodType=640b3f9df9c3ad4c73ee6dee' \
  --data-urlencode 'priceType=66ec3c0d839b0ba2569a3d60' \
  --data-urlencode 'rateType=66ec407a839b0ba2569a3d68' \
  --data-urlencode 'paymentPlan=640b411b1a4b8b502377ecc7' \
  --data-urlencode 'paymentType=666b041e57465219254fdfce' \
  --data-urlencode 'renewableContract=6405cb2d1891597d4ac7abdd' \
  --data-urlencode 'transactionHorodate=2024-09-20 11:20:20.043Z' \
  --data-urlencode 'lastname=Huet' \
  --data-urlencode 'firstname=Fabien' \
  --data-urlencode 'birthdate=02/08/1984' \
  --data-urlencode 'placeOfBirth=Clamart' \
  --data-urlencode 'address=7 avenue de la Mazure' \
  --data-urlencode 'zipcode=50810' \
  --data-urlencode 'city=La Barre de Semilly' \
  --data-urlencode 'country=France' \
  --data-urlencode 'phone=0698915707' \
  --data-urlencode 'email=fabien.huet@gmail.com' \
  --data-urlencode 'idTransaction=617812616943d08d525aec6f' \
  --data-urlencode 'countryOfBirth=France'

That should get us something like:

{
  "success": true,
  "status": 200,
  "response": {
    "message": "New adhesion created id 66ed4ab9d7a62a3cb800d18c",
    "id": "66ed4ab9d7a62a3cb800d18c",
    "compliance": true
  }
}

Where to get the fields:

Once this is done, the customer gets an email from Owen to activate their account.

idTransaction format: IBANXXXXX-ddmmyy-firstName-lastName

TODO

Get adhesions

For reconcilliation or verification purposes, we might want to get the adhesions data from Owen.

To get one adhesion :

curl --location '{{host}}/adhesions/{{adhension_id}}' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --header 'Authorization: Bearer {{token}}'

That should give us something like:

{
  "success": true,
  "status": 200,
  "response": {
    "adhesion": {
      "adhesionStatus": "active",
      "activationDate": "2024-09-20T09:39:37.965Z",
      "_id": "66ed42d9d7a62a3cb800cf1e",
      "contractType": {
        "timeComplianceChecksMax": 0,
        "_id": "66ec3637839b0ba2569a3d18",
        "name": "green-got",
        "description": "green-got",
        "idContractType": 630718001
      },
      "channel": {
        "_id": "666b030857465219254fdfad",
        "name": "Direct"
      },
      "contractPeriod": {
        "_id": "640b3f21f9c3ad4c73ee6d24",
        "name": "1 year",
        "unit": "year",
        "value": 1
      },
      "contractPeriodType": {
        "_id": "640b3f9df9c3ad4c73ee6dee",
        "name": "fixed period"
      },
      "priceType": {
        "_id": "66ec3c0d839b0ba2569a3d60",
        "name": "Default price green-got"
      },
      "rateType": {
        "_id": "66ec407a839b0ba2569a3d68",
        "name": "Carte green-got essentiel",
        "description": "Carte green-got essentiel",
        "rate": 0,
        "unit": "eur"
      },
      "paymentPlan": {
        "_id": "640b411b1a4b8b502377ecc7",
        "name": "year"
      },
      "paymentType": {
        "_id": "666b041e57465219254fdfce",
        "name": "Direct"
      },
      "renewableContract": {
        "_id": "6405cb2d1891597d4ac7abdd",
        "name": "tacite agreement"
      },
      "transactionHorodate": "2024-09-20T11:20:20.043Z",
      "lastname": "Huet",
      "firstname": "Fabien",
      "birthdate": "1984-08-02T00:00:00.000Z",
      "placeOfBirth": "Clamart",
      "address": "7 avenue de la Mazure",
      "zipcode": "50810",
      "city": "La Barre de Semilly",
      "country": "France",
      "phone": "0698915707",
      "email": "fabien.huet@gmail.com",
      "idTransaction": "617812616943d08d525aec6f",
      "countryOfBirth": "France",
      "cancelation": false,
      "productName": "green-got",
      "createdAt": "2024-09-20T09:39:37.975Z",
      "lastStatusUpdate": "2024-09-20T09:39:37.976Z"
    }
  }
}

To get them all :

curl --location '{{host}}//list-all-adhesions?page=2&limit=2' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --header 'Authorization: Bearer {{token}}'

That gets us a list of adhesions. This call is paginated and ends with the total amount.

Get products

We can start by getting our contract_id with the products/contract_type.

curl --location '{{host}}/owen-products/contract_type' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --header 'Authorization: Bearer {{token}}'

That should get us something like:

{
  "success": true,
  "status": 200,
  "response": {
    "contractTypes": [
      {
        "_id": "66ec3637839b0ba2569a3d18",
        "name": "green-got",
        "description": "green-got",
        "idContractType": 630718001,
        "url": "api.get-owen.com/owen-products/contract_type/66ec3637839b0ba2569a3d18"
      }
    ]
  }
}

Here we take the first element in the contractTypes array and then the_id.

This is an array for technical reasons, but it should stay with only one line pretty much for ever.

We can save the contract_id in the env variables to avoid future call to this endpoint.

The we can get the products with

curl --location '{{host}}/owen-products/contract_type/{{contract_id}}' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'x-api-key: {{key}}' \
  --header 'Authorization: Bearer {{token}}'

That should give us something like:

{
  "success": true,
  "status": 200,
  "response": {
    "channels": [
      {
        "_id": "666b030857465219254fdfad",
        "name": "Direct"
      }
    ],
    "contractPeriodType": [
      {
        "contractPeriod": [
          {
            "_id": "640b3f21f9c3ad4c73ee6d24",
            "name": "1 year",
            "unit": "year",
            "value": 1
          }
        ],
        "_id": "640b3f9df9c3ad4c73ee6dee",
        "name": "fixed period"
      }
    ],
    "marketingCountry": [
      {
        "description": "this contract can be marketed in France",
        "name": "France"
      }
    ],
    "paymentPlan": [
      {
        "_id": "640b411b1a4b8b502377ecc7",
        "name": "year"
      }
    ],
    "paymentType": [
      {
        "_id": "666b041e57465219254fdfce",
        "name": "Direct"
      }
    ],
    "priceType": [
      {
        "rateType": [
          {
            "_id": "66ec41bc839b0ba2569a3d6d",
            "name": "Carte green-got premium",
            "description": "Carte green-got premium",
            "rate": 0,
            "unit": "eur"
          },
          {
            "_id": "66ec407a839b0ba2569a3d68",
            "name": "Carte green-got essentiel",
            "description": "Carte green-got essentiel",
            "rate": 0,
            "unit": "eur"
          }
        ],
        "_id": "66ec3c0d839b0ba2569a3d60",
        "name": "Default price green-got"
      }
    ],
    "productNameTranslation": [],
    "renewableContract": [
      {
        "_id": "6405cb2d1891597d4ac7abdd",
        "description": "insurance contract is renewed automatically every year",
        "name": "tacite agreement"
      }
    ],
    "signature": [],
    "withdrawalPeriod": [
      {
        "_id": "648ff7c89a6a1b77fc40ef4c",
        "name": "30 days"
      }
    ],
    "mandatoryFields": [
      {
        "name": "contractType"
      },
      {
        "name": "contractPeriod"
      },
      {
        "name": "lastname"
      },
      {
        "name": "firstname"
      },
      {
        "name": "birthdate"
      },
      {
        "name": "placeOfBirth"
      },
      {
        "name": "address"
      },
      {
        "name": "zipcode"
      },
      {
        "name": "city"
      },
      {
        "name": "country"
      },
      {
        "name": "phone"
      },
      {
        "name": "email"
      },
      {
        "name": "contractPeriodType"
      },
      {
        "name": "renewableContract"
      },
      {
        "name": "priceType"
      },
      {
        "name": "rateType"
      },
      {
        "name": "paymentPlan"
      },
      {
        "name": "paymentType"
      },
      {
        "name": "channel"
      }
    ],
    "subscriptionPortalDescriptions": [],
    "_id": "66ec3637839b0ba2569a3d18",
    "name": "green-got",
    "description": "green-got",
    "idContractType": 630718001
  }
}

All those data will be used to create an adhesion.

TODO

Accounts & cards

Account products

Overview:

The periods for billing are not the first/end of the month. You can start a period on any day.

Product Data Structure:

Two sibling products must have the same debit_time. This must be checked manually.

About the debit time

Products can be charged at the end of their period. They are end_of_period products. Or at the beginning of their period, right when the subscription is validated. They are begining_of_period products.

Product Visibility

Product management

Products are defined during workshop. The product commity decides the characteristics of the product and then the product is manually inserted in the database or in the code by the ops team. Products are supposed to be mostly immutable. Perks might change with time. But the price and the base definition should never change.

Availability to customers

This domain logic module check the availability of a product for a specific customer.

The availability depends on the internal characteristics of the product and of the end customer characteristics.

Rules :

Products List

Individual current account legacy

Sole trader account legacy

Shared Account legacy

Shared Account additional card holder legacy

Individual current account essential monthly version 2024/12/01

Individual current account essential yearly version 2024/12/01

Individual current account premium monthly version 2024/12/01

Individual current account premium yearly version 2024/12/01

Individual current account ultra monthly version 2024/12/01

Individual current account ultra yearly version 2024/12/01

Shared current account essential monthly version 2024/12/01

Shared current account essential yearly version 2024/12/01

Shared current account premium monthly version 2024/12/01

Shared current account premium yearly version 2024/12/01

Shared current account ultra monthly version 2024/12/01

Shared current account ultra yearly version 2024/12/01

Shared current account essential monthly version 2024/12/01

Shared current account additionnal card holder essential monthly version 2024/12/01

Shared current account additionnal card holder essential yearly version 2024/12/01

Shared current account additionnal card holder premium monthly version 2024/12/01

Shared current account additionnal card holder premium yearly version 2024/12/01

Shared current account additionnal card holder ultra monthly version 2024/12/01

Shared current account additionnal card holder ultra yearly version 2024/12/01

Shared current account additionnal card holder essential monthly version 2024/12/01

Sole trader current account essential yearly version 2024/12/01

Sole trader current account premium monthly version 2024/12/01

Sole trader current account premium yearly version 2024/12/01

Sole trader current account ultra monthly version 2024/12/01

Sole trader current account ultra yearly version 2024/12/01

Account cancellations

Cancellations originate from the end customer.

TODO

Account closing

Closings originate from green-got. Reasons for closing an account include compliance issues and overdraft situations.

Cancellation data structure:

Proposed workflow:

  1. Initiation: in the back office, the officer creates the request with initial data
  2. Review:
  1. Notification: account holder is notified of account closure
  2. Account Freezing: restrict further transactions on the account
  3. Balance Settlement:
  1. Update integrations
  1. Archive the customer case

TODO

Back office listing

An API endpoint allows the back office to see all the products. Expired or available.

Change billing period

If the current product has a billing_period_sibling, the customer can switch to this new period.

If the customer switches to a longer period, we close the current subscription, create a credit note and create a new subscription for the longer product. If the debit_time is begining_of_period, we must check that the account holder has enough money on their bank account.

If the customer switches to a shorter period, he already payed for the long period and we don’t reimburse. We setup a workflow and at the next end of period, we cancel the subscription before they pay and create a new subscription.


If the current product does not have a billing_period_sibling and the account holder is switching from an end_of_period to a begining_of_period. We charge the customer pro rata temporis for the ellapsed time of the end_of_period then we cancel the subscription and create a new one for the new product selected.


If the current product does not have a billing_period_sibling and the account holder is switching from an begining_of_period to a end_of_period. We cancel the current subscription, create a credit note pro rata temporis and create a new subscription.

TODO

Customer debit for subscription

We have a daily cron job to check if a subscription debit for an end customer must happen this day.

We check that the subscription still exists. The customer may have switched to a shoter period. (see Change billing period)

Process Steps:

  1. Price calculation: Final Price = Base Price - Discounts - Credit Notes
  1. Internal Transfer: If a debit is needed, an internal transfer is made to Green-Got.

TODO

Examples

Discounts

Overview:

Data structure for discounts:

Creation workflow in the back office

  1. Marketing team member logs into the discount management system
  2. Selects “Create New Discount” option
  3. Fills in the required fields from the data structure
  4. Submits the discount for creation

Technical Workflow:

  1. System receives discount creation request
  2. Validates all input fields:
  1. If validation fails, returns error messages to the customer
  2. If validation passes, creates new entry in the discount database
  3. Generates a unique ID for the discount
  4. Returns success message and discount details to the customer

TODO

Credit notes

Credit notes can be issued for various reasons, including:

Credit Note Lifecycle:

Credit Note Structure:

TODO

  1. Define the detailed workflow for:
    • Credit note creation
    • Application (manual and automatic)
    • Partial usage and recreation
    • Expiration handling (if applicable)
  2. List and define all possible reasons for credit note creation, such as:
    • Overpayment refund
    • Service disruption compensation
    • Loyalty reward
    • Downgrade compensation
    • Billing error correction
    • (Add other relevant reasons)
  3. Determine and document:
    • Approval process for credit note creation (if any)
    • Reporting and auditing requirements
    • Integration with other financial systems
Manual creation

The compliance, marketing, and customer care teams have the authority to create credit notes for commercial management purposes.

Customer workflow:

  1. Customer Selection: Operator selects the specific customer account.
  2. Action Initiation: Operator chooses the “Add a Credit Note” action.
  3. Reason Selection: Operator selects the appropriate reason for issuing the credit note.
  4. Message Addition (Optional): Operator may include an explanatory message if needed.

TODO

End customer products list

An internal feature and an API endpoint for the customer to know all the producs available to them.

Fees

Fees are taken on the ci.

Credit notes apply to the fees.

Fees are added in the next monthly invoice.

Invoicing

We have a monthly cron for each account. On each account, we add the money taken for the subscription and the money taken from the fees.

Invoices can be at zero for a yearly subscription.

One line per item.

Item must indicate the discounts and credite notes applied.

  1. Invoice Creation: An invoice is inserted in the database for the transaction ; but the pdf is not generated.
  2. Invoice Distribution: If applicable per the product definition, the invoice is sent to the customer via a notification email. This will most likely be the case for business customer that need it for their accounting. In this case, the pdf is generated.
  3. Transaction Attachment: The invoice is attached to the transaction record in the database for easy customer access. This will most likely be the case for business customer that need it for their accounting. In this case, the pdf is generated.

Overdraft

TODO

Product debit warning

We have a daily cron job to check for an upcoming subscription debit for end customers.

Process Steps:

  1. Daily Check: The cron job runs every day to determine if a debit is coming in the next 7 days.
  2. Funds Verification:
  1. Yearly Commitment Notification: For customers with a yearly commitment, an email notification is sent once, one week in advance. This email informs them of the upcoming debit and reminds them that if they do not cancel their subscription, they will be charged for another year with no refund possible.

TODO

Product downgrade

When the customer has a product and they want to downgrade to a cheaper product. If they have a product that they have already payed for, the current subscription will run untill the end of it’s time and the new downgraded subscription will start. Otherwise, the current subscription stays active until the end of that period. At the end, the subscription is canceled and switched to the lower-tier product.

Downgrading doesn’t order a new card. You can order a new card for the price of a reorder if you want to.

From a product charged at the beginning of the subscription period

In the case where a customer has a product charged at the beginning of a period, downgrading to a cheaper product keeps the current subscription active until the end of that period. At the end, the subscription is canceled and switched to the lower-tier product.

No reimbursements are provided.

Workflow

  1. Check for an existing downgrade_pending status. If one exists, stop the process here.
  2. Change the status of the current subscription to downgrade_pending.
  3. Create a scheduled workflow to execute at the end of the current subscription period:
    • Create a new subscription for the target product. If it is a beginning-of-the-month charging product, charge the customer immediately. Assign the predecessor ID of the previous subscription to this new subscription.
    • Close the current subscription, updating its status to closed and assigning it a successor ID linked to the newly created subscription.
    • Send an email to recap the switch.

Example: Happy path

Arrange

Act

Assert

Arrange

Assert

Example: Currently downgrading

Arrange

Act

Assert

From a product charged at the end of the subscription period

In case where a customer has a product that is billed at the end of a period, if they choose to downgrade to a cheaper product, the following process occurs: the current subscription ends immediately, the customer is charged pro rata for the time used, and a new subscription is created.

Workflow

  1. Calculate Charges: Determine the amount to charge for the elapsed time and the new subscription, especially if the downgrade occurs at the beginning of the month.
  2. Verify Funds: Check that the customer has sufficient funds in their bank account to cover the pro-rated charge. If insufficient, halt the process and notify the customer of the error.
  3. Charge for Elapsed Time: Process the charge for the time the customer has already used.
  4. Close Current Subscription: Mark the current subscription as closed and assign it a successor ID.
  5. Create New Subscription: Set up a new subscription and link it to the previous subscription with a predecessor ID.
  6. Immediate Charge (if applicable): Charge the customer immediately for the new subscription if it is warranted.
  7. Send Confirmation Email: Send an email to the customer summarizing the changes made during the downgrade process.

Example: Happy path

Arrange

Act

Assert

Example: Happy path alternative

Arrange

Act

Assert

Example: Not enough money

Arrange

Act

Assert

Product subscription

When customers subscribe to a product, we create a subscription and we charge them on the spot if the debit_time is begining_of_period.

Subscription model

TODO

Product subscription status

Subscription status

We define an enum SubcriptionStatus that exists in the subscription and that reflect the current status of this subscription.

Values :

Product upgrade

When customers change to a more expensive product, the previous subscription is canceled, and a new subscription is created.

The customer is charged on the spot for a product with a begining of period payment. (see Product upgrade credit note calculation).

Upgrading doesn’t order a new card. You can order a new card for the price of a reorder if you want to.

From a product charged at the begining of the subscription period

When the customer has a product that is charged at the beginning of the subscription period and they upgrade for a more expensive product.

Workflow

Example: Happy path to a begining of period payment

Arrange

Act

Assert

Example: Happy path to an end of period payment

Arrange

Act

Assert

Example: Out of money

Arrange

Act

Assert

Example: Currently downgrading

Arrange

Act

Assert

From a product charged at the end of the subscription period

When the customer has a product that is charged at the end of the subscription period and they upgrade for a more expensive product.

In practice, the only products charged at the end of the subscription period are the legacy products.

Workflow

Example: Happy path to a begining of period payment

Arrange

Act

Assert

Example: Happy path to an end of period payment

Arrange

Act

Assert

Example: Out of money

Arrange

Act

Assert

Example: Currently downgrading

Arrange

Act

Assert

Products bundles

Bundle pricing have to be displayed on subscription as a discount.

They are managed as permanent discounts.

Investment

Life insurance

Savings

Reporting

Business intelligence

Customers

Financial

Operational

Regulatory

Security

Alerting

Data protection

Encryption

Keys management

Monitoring

System

Administration

Audit trail

Employee management

Parameters

Role based access control

Requirements

Auditability

Availability

Data retention

Disaster recovery

Performance

Scalability

Technical architecture

Database design

Deployment

Integrations

Network

Service calls

Depending on the requirements, service calls may have different characteristics:

Synchronous call

Query example: I want to get some data about a particular transaction.

Flow:

  1. the API calls the domain usecase get_transaction_detail
  2. the domain calls the store transaction
  3. the store make a query to the database in SQL
  4. the response bubles back to the API

If there is an error there we just return an error.

Command example: create an invoice

Flows:

  1. the API calls the domain usecase create_invoice with a payload
  2. the domain starts a transaction
  3. the store is called for update 1 ; the store calls the db
  4. the store is called for update 2 ; the store calls the db
  5. the store is called for update 3 ; the store calls the db
  6. the transaction is commited
  7. the response bubble to the client

If any of the store calls fail, the transaction is canceled and an error is thrown.

Workflow (temporal)

Create example: I want to create a recurring invoice

  1. the API call the domain usecase create_recurring_invoice with a payload
  2. the domain starts a transaction
  3. insert the recurring invoice to the database
  4. send the workflow characteristics (arguments and temporality) to postgres for batch treatment
  5. the response bubbles to the client
  6. workflows from postgres are batch treated and sent to temporal cloud (arguments are encrypted)
  7. later on, temporal will trigger the execution of the activity

Cancel example: I want to stop a recurring invoice

  1. the API calls the domain usecase cancel_recurring_invoice with a payload
  2. the domain starts a transaction
  3. remove the recurring from the database
  4. send the workflow characteristics (arguments and temporality) to postgres for batch treatment
  5. the response bubbles to the client
  6. workflows from postgres are batch treated and sent to temporal cloud (arguments are encrypted)

Queue

Sent to postgres queue

Priority Queue

TODO

Cold Queue

TODO

System architecture

Temporal

Temporal.io

Setup

You need to have the temporal-dev cluster running. You can then access the Web UI at http://localhost:8233.

SDK

The repo uses the official Temporal Rust crates through the temporal crate.

src/temporal provides:

Worker

Each instance of the monolith is a worker that listens to the temporal-dev cluster and executes workflows and activities.

The shared worker registration is in src/services/temporal/src/temporal_worker.rs. Crates that own workflows and activities expose register_temporal_worker(...) helpers, and the service worker composes them.

Workflow

The workflows are located in the domain package, next to the business logic

You can find them in the workflows directory.

Define a new workflow

Workflows use the official macro-based API.

use temporal::{WorkflowContext, WorkflowResult, workflow, workflow_methods};

#[workflow]
pub struct SendNotificationWorkflow;

#[workflow_methods]
impl SendNotificationWorkflow {
    #[workflow_method]
    pub async fn run(
        ctx: WorkflowContext<Self>,
        args: SendNotificationArgs,
    ) -> WorkflowResult<()> {
        let recipient_id = ctx
            .activity(
                "SendNotification",
                args,
                ActivityOptions::default(),
            )
            .await?;

        ctx.timer(Duration::from_mins(15)).await;

        ctx.activity(
            "StoreNotification",
            recipient_id,
            ActivityOptions::default(),
        )
        .await?;

        Ok(())
    }
}

Register the workflow

Register workflows in the crate-owned register_temporal_worker(...) helper and compose that from the service worker.

pub fn register_temporal_worker(
    mut worker_options: temporal::WorkerOptions,
) -> temporal::WorkerOptions {
    worker_options.register_workflow::<SendNotificationWorkflow>();
    worker_options
}

Start a workflow

Start workflows with the client.

client
    .start_workflow(
        SendNotificationWorkflow::run,
        SendNotificationArgs { user_id, notification },
        WorkflowStartOptions::new("default", workflow_id).build(),
    )
    .await?;

Parameters

Workflow inputs are normal function arguments on run.

pub async fn run(
    _ctx: WorkflowContext<SendNotificationWorkflow>,
    args: SendNotificationArgs,
) -> WorkflowResult<()> {
    // ...
}

Tests

The old in-process mock-worker test harness was removed with the migration. Temporal workflow tests now need integration-style coverage against the official SDK/runtime.

Activity

Activities also use the macro-based API.

Create a new activity

Define an activities implementer and annotate methods with #[activity(...)].

Start an activity

Start activities from workflow context by activity name and input payload.

let recipient_id = ctx
    .activity("SendNotification", args, ActivityOptions::default())
    .await?;

Parameters

Activity inputs are normal function arguments.

use std::sync::Arc;
use temporal::{ActivityContext, ActivityError, activities};

pub(crate) struct HttpActivities;

#[activities]
impl HttpActivities {
    #[activity]
    pub(crate) async fn http_request(
        self: Arc<Self>,
        _ctx: ActivityContext,
        args: HttpRequestArgs,
    ) -> Result<String, ActivityError> {
        // ...
    }
}

Register the implementer with worker_options.register_activities(...).

Transactions

Purpose

Scope

Chargeback

Internal

From a GG account to a GG account

Customer to GG

Customer to customer

GG to customer

Ledger

Terms and general notions

Note: how we manage raw events and the normalized ledger is still to be precised. We might put in the normalized ledger everything that is necessary for the banking part, but store elsewhere what is linked to the transaction for account, marketing, etc purposes, like notes on transactions, categorization, co2 intensity etc. This way some cosmetic transaction enrichment feature has no risk to break something in the banking part.

The raw events and the evented ledger are the source of truth and are immutable. Their conjunction enables to generate (or regenerate) the normalized ledger.

Transactions in the normalized ledger are mutable: they evolve over their lifecycle.

Balances

We will have 6 fields to represent the balances

The pending balances are just used during the time where the authorization checks are performed.

From this we should be able to calculate:

Evented ledger

The money movements described in the evented ledger might come from very diverse sources. To make things simpler and avoid errors, we store the bare minimum to describe money movement between accounts.

Examples

To present the different cases, and entry types, here are three transactions. Left part is the evented ledger (without all columns for simplicity), right part the balances (just the debits for simplicity). The Δ columns are just here to emphasize the impact of each entry type on the different balance types, they won’t be in the DB.

Here the transaction with id 1 shows a transaction authorized and cleared (with a different amount), the 2 shows a transaction where authorization is rejected and 3 and transaction expired or canceled at clearing.

txn idamounttype|Debits clearedΔDebits authorizedΔDebits pendingΔCredits clearedΔBalance clearedBalance availableBalance for limits
|200010402020
110RESERVED|200001010400202010
110AUTHORIZED|20010100-10400201010
112COMPLETED|32120-1000400888
22RESERVED|3200022400886
22REJECTED|320000-2400888
36RESERVED|3200066400882
36AUTHORIZED|320660-6400822
36CANCELED|3200-600400888

Relationship between entries and balances

The evented ledger represents transactions as a three step process: a reservation, then an authorization or rejection, followed by a completion or cancellation. If a transaction is refunded, then from the point of view of the evented ledger, the refund is another transaction.

The authorization checks : balance, limits, geolocation, black list etc are done between steps 1 and 2.

No matter what the context is (one phase, two phase transactions), we want from the type of entry to the ledger to know which balances it is impacting. Here is the detail:

  1. Reservation, this event:
  1. a. Authorization, this event:
  1. b. Rejection, in which case there is no step 3. This event:
  1. a. Completion, this event:
  1. b. Cancellation, this event:

Ledger entities

A ledger can only hold one currency. As we might want to enable having several currencies in the future, we define ledger entities. Every account belongs to one ledger only.

Entries

Entries are describing a money movement or reservation:

Transaction

A transaction links the ledger entries of one transfer of money. This table will contain the following fields:

We will have a unicity constraint on ledger_id, external_id, scheme, source_account, destination_account.

Mastercard or SEPA might give the same transfer id for the source and destination (to confirm). So if it’s a SEPA or MC payment between two of our clients, we might have two different events with the same id as a debit and credit. Having source and destination as part of the unique key enables to differentiate these two transactions (outgoing vs incoming).

In the rare case where source/destination accounts would change between the authorization and clearing, having both these fields as part of unicity should enable to work the following way:

We don’t want to put directly a transaction id from an external source in our entries, we want it to be our own id. Having a table dedicated to linking our own id to the external ones enables to avoid (unlikely) collisions for our own transaction ids with a unique index.

Note on concurrency

Special care must be taken to handle concurrent financial movements. For example, if the available balance is 100€ and we receive 2 payments of 100€ at the same time, we cannot accept both. The most straightforward solution to this is to lock the account during the whole payment process, so that only one payment can be processed at a time. This is the easiest to manage, but for accounts with a lot of traffic, we risk queueing and timeouts.

We wanted to avoid locking the account during the processing, so we rely instead on a different solution. When receiving a payment request, we will update the balance at the start of the process, before deciding if we accept it or not. If we reject, we update the balances back.

The important point is that no lock is kept, and several payments can be processed at the same time. The downside is, in some rare cases, we may refuse some payments we could have accepted.

Note that this design requires the balance updates to be visible by all database clients immediately, i.e. they cannot be part of a database transaction (updates in DB transactions are only visible to others after commit).

Flows of the evented ledger

Later I will add the flows as mermaid charts here, but as it might still evolve, it makes more sense to have it on Figma for now, so please look this link: https://www.figma.com/board/heJxL59c2t6DRiFEYKiJ0S/New-backend-design?node-id=891-790&t=mHYXWLvdBChieH3t-1

One thing expressed on this flows is that there should never be an entry in the ledger without its previous steps. As an example, a completion entry without reservation and authorization entries. We can imagine the unlikely case where the clearing event would arrive before the reservation event. As we would not see the entries already, we would directly add to the entries of the ledger the three entries: reservation, authorization, clearing. If this insert fails, we know that there was a concurrent insert of a reservation or authorization or both, without having to do any lock. To be able to make the insertion fail in that case, we can add a field on the entries of the ledger called steps (1, 2, 3), and a unique index (transaction id, step).

Sources

Double-entry bookkeeping general info

Separating balances (debit/credit)

Model with two separate entries in the evented ledger for credit & debit: https://docs.moderntreasury.com/platform/reference/ledger-entry-object

… model with one entry: https://docs.tigerbeetle.com/reference/transfer/#modes Source code here

Reserving funds for entries: https://docs.tigerbeetle.com/coding/two-phase-transfers/

Some model inspiration for accounts, with the external id: https://docs.moderntreasury.com/platform/reference/ledger-transaction-object

Notes

To follow double entry bookkeeping, every transaction is a debit to an account and a credit to another. So for incoming or outgoing transactions we will have some accounts that represent the outside.

We first considered a “two step” evented ledger, with only reserved, completed, canceled entries types, and pending and cleared balances. The “balance available” shown to the user would then be cleared credit - cleared debit - pending debit. Problem is that during the short time where the checks are done, the balance of the user would be affected, even for transactions that are impossible for their balance. So it would make it harder to identify when an account actually goes negative. With these three step process, for a user account the available balance should never be negative, but the balance for limits might.

For later reference: on this abandoned two step version, two flows were proposed for authorization. From our discussions on the pros and cons, we were more interested in going for the version without lock. The disadvantage that a limit might be reached in case of concurrency because of a transaction that will end up being refused should not be a big deal: concurrency might happen only on technical accounts, and for those accounts, limits are not so important (or maybe even not checked). Keeping a trace of rejected Mastercard authorizations instead of having it just rolled back and not appearing in the evented ledger can also simplify things, since we want these failures in the normalized ledger.

Mastercard

Authorisation

Data collection

Data validation

Response

Chargeback

Conditions

Process

Required informations

Timeframe

Fees

Dispute

Refund

Transaction

Withdrawal

ISO 8583

Errors

Fields

Message structure

Message types

Monitoring

Alerting

Post transaction

Real-time

Refunds

Conditions

Process

Required informations

Model

Errors

Error codes and messages.

Events

State transitions and their implications.

Statuses

pending, completed, failed, canceled…

SEPA

Summary

See the ring fencing procedure to have all the details.

We are collaborating with Arkea to provide SEPA and banking capabilities.

The Arkea service consists of handling the flows acquired or received, processing them, routing them and accounting for them (customer account and general accounting). Since Green-Got has a simple payment establishement licence, we cannot have a bank account at the ECB and cannot be direct participants. Arkea manages the seggregation and settlement account and is our direct participant to SEPA.

The seggregation account is a large pocket of liquidity holding all the deposits of our customers. It exists to make sure that we do not use our customers money for ourself. To seggregate their assets from ours.

The settlement account is a smaller pocket of liquidity that is used to settle payement with other banks. When a customer receives money from another bank, it goes to the settlement account and we have to move it as soon as possible to the seggregation account. When we want Arkea to send money for an SCT out, we need to put this money on the settlement account first.

Green-Got holds the ledger. We say what money belong to whom in the seggregation account. We also hold the customer list and we generate the outgoing fluxes. When a customer wants to send money, we send a transaction file to Arkea.

When Arkea receives funds for one of our clients, they send us the opration and the money is on our settlement account.

We are connected to their flux management system. This flux management system is connected to their routing system. Their routing system is connected to the CSM ; STET and EBA.

20022 integration

The ISO 20022 is defined here: https://www.iso20022.org/iso-20022-message-definitions

Arkéa implement the raw rulebook without encapsulation.

We use a subset; including some:

SCT (SEPA Credit Transfert)

NGRCSCT (Negative Recall)

camt.029

Order

pacs.008

PORCSCT (Positive Recall)

pacs.004

RCSCT (Recall)

camt.056

RTSCT (Return)

pacs.004

SDD (SEPA Direct Debit)

Order

pacs.003.psp

RFSDD (Refund)

pacs.002

RJSDD (Reject)

pacs.004

RTSDD (Return)

pacs.004

Beneficiaries

Add

Edit

Model

Remove

VOP

Verification of Payee (VOP)

Verification of Payee is the payee-name check that PSPs must offer for SEPA credit transfers under the EU Instant Payments Regulation: before a transfer is sent, the payer’s PSP checks that the name the payer entered matches the holder of the destination IBAN, and returns a match / close-match / no-match result so the payer can decide before paying. Its purpose is fraud prevention (APP fraud, misdirected payments).

Our provider: GGBS

Green-Got does not run the VOP infrastructure itself — we use GGBS as our VOP provider. GGBS sits between us and the wider VOP network and the EPC directory. There are two directions, and Green-Got is involved in both.

1. Requester mode — we ask (outbound)

When our customer adds a beneficiary or initiates a transfer, we call GGBS to verify the beneficiary name against the destination IBAN. GGBS returns a match outcome:

OutcomeMeaning
MTCHThe name matches the account holder.
CMTCClose match — the holder’s actual name is returned so the customer can confirm.
NMTCNo match — the name does not correspond to the account holder.
NOAPNot applicable / not verifiable — the destination bank could not be reached or does not participate.

GGBS also enriches the response with IBAN validation and FNC-RF fraud-list signals. How we act on each outcome when adding a beneficiary is covered in VOP – Internal and VOP – External.

2. Managed (responder) mode — we answer (inbound)

When someone else is about to pay one of our customers, the payer’s PSP runs a VOP check against our customer’s Green-Got IBAN. That request reaches GGBS, and GGBS — in managed mode — calls our responder to ask “who can be paid at this IBAN?”. GGBS then does the name matching against what we return.

What we return as payee identities

For a given IBAN, the payee identities are:

  1. the account’s legal holder — the natural person (personal account) or the legal entity / company (business account); always returned; and
  2. any participant flagged as a named account party — a co-titulaire or mandataire whose name a remitting bank may legitimately pay.

Operational participants are never returned. An accountant, a viewer, or an employee with day-to-day access is not a payee — returning their name would let a transfer addressed to them match an account they merely operate, which would weaken VOP. Concretely:

The holder/participant model and the “named account party” flag are described in Account access.

External
Internal

When we try to add a beneficiary that already has a Green-Got account, they become internal beneficiaries.

The name that the customer enters must correspond exactly to the first_name last_name or last_name first_name on the bank detail.

Rule for shared accounts : the name must correspond to ONE OF the first_name last_name or last_name first_name on the bank details.

Which names an account exposes (managed responder)

When the payee account is a Green-Got account, GGBS resolves the holder names by calling our managed responder (see VOP). The “names on the bank details” that an account exposes are:

So the “ONE OF” rule above is, concretely, “one of the holder + named-party participant names”. For a business account this is normally just the company name; for a shared personal account it is the holder plus any flagged co-titulaire. The set of names and the flag are defined in Account access.

We format the name to slugify:

CFT

Chronology

How thing happen chronologically speaking

EBICS

Fees

IBAN

Each account in the SEPA network is identified by an IBAN.

IBAN Structure

An IBAN (International Bank Account Number) is defined by ISO 13616 and has the following structure:

FR76 1792 8999 9999 0434 9325 8883
^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
||   Account number (country-specific)
||
|+-- Check digits (2 digits, numeric)
+--- Country code (2 letters, ISO 3166-1 alpha-2)

Components

PositionLengthContentExample
1–22 charsCountry code (ISO 3166-1)FR
3–42 digitsCheck digits (MOD-97-10)76
5–endvariesBBAN (Basic Bank Account Number)17928999999904349325888

BBAN — France (23 chars)

France’s BBAN follows the RIB format:

FieldLengthExample
Bank code5 digits17928
Branch code5 digits99999
Account number11 alphanumeric90434932588
RIB key2 digits83

Branch codes

Our branch codes are published to the “Banque de France”, to be used this way:

Branch codePurpose
00000Internal (for our tests)
00001For GG operational needs: master account, …
00002Retail customers
00003Business customers
00004Institutional partners
99999Unit tests (not published)

Monitoring

Alerting

Manual validation

Post transaction

Real time

PSR

Refund

Reversal

SCT

In

Out

Daily cutoff
Order list

SCT Inst

In

Out

SDD

B2B

Core

SWIFT

FOREX

Fees