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:
- Provide payment services as defined in Article L.314-1 of the French Monetary and Financial Code
- Execute payment transactions
- Issue and manage payment instruments
- Manage payment accounts
- Ensure compliance with French and European regulatory requirements
Scope
In Scope:
- Customer onboarding and management in accordance with French regulations
- Payment account management
- Payment transaction processing (domestic and international)
- Payment instruments issuance and management
- Real-time payment processing capabilities
- Regulatory compliance monitoring and reporting
- Integration with French and European payment systems though Arkea and MasterCard
- AML/KYC processes aligned with ACPR requirements
- Reporting to regulatory authorities (ACPR, Banque de France)
- Security and fraud prevention measures
- SEPA payments processing (SCT, SDD)
- Card payments processing
- Third party provider for saving accounts and insurances.
Out of Scope:
- Credit services
- Investment services
- Interest-bearing accounts
- In house banking products (savings accounts, term deposits)
- Trading services
- In house insurance products
Regulatory framework
We must comply with a multi layered system of regulations
European Union Level Regulations
Payment Services
- PSD2 (Directive (EU) 2015/2366): eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32015L2366
- PSD3 (In development, expected implementation 2024-2025): eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CONSIL:ST_11222_2023_INIT&qid=1732866078975
- Strong Customer Authentication (RTS): eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32018R0389
- Markets in Crypto-Assets (MiCA): eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32023R0858
Data Protection
- General Data Protection Regulation (GDPR): gdpr.eu/
- ePrivacy Directive: eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:32002L0058
- Digital Operational Resilience Act (DORA): eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022R2554
Anti-Money Laundering
- AMLD6 (Directive (EU) 2018/1673): eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32018L1673
- Wire Transfer Regulation 2015/847: eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32015R0847
French National Regulations
Regulatory Bodies
- ACPR (Autorité de Contrôle Prudentiel et de Résolution): acpr.banque-france.fr/
- AMF (Autorité des Marchés Financiers): www.amf-france.org/
- Banque de France: www.banque-france.fr/
- CNIL (Commission Nationale de l’Informatique et des Libertés): www.cnil.fr/
Legal Codes
- Code Monétaire et Financier: www.legifrance.gouv.fr/codes/texte_lc/LEGITEXT000006072026/
- Code de Commerce: www.legifrance.gouv.fr/codes/texte_lc/LEGITEXT000005634379/
- Code de la Consommation: www.legifrance.gouv.fr/codes/texte_lc/LEGITEXT000006069565/
Industry Standards and Security Requirements
- PCI DSS 4.0 (Payment Card Industry Data Security Standard): www.pcisecuritystandards.org/
- ISO 20022 (Payment messaging standards): www.iso20022.org/iso-20022-message-definitions
- ISO 8583 (Card transactions messaging standard): en.wikipedia.org/wiki/ISO_8583
- SWIFT Standards: www.swift.com/standards
Payment Schemes and Infrastructure
- SEPA (Single Euro Payments Area) www.europeanpaymentscouncil.eu/about-sepa/sepa-political-legal-and-regulatory-framework
- Mastercard developer.mastercard.com/mastercard-processing-debit/documentation/
Key stakeholders
Internal Stakeholders
Management
- Board of Directors
- Executive Committee (founders)
Operational Teams
- Customer support
- Transaction Processing & Reconciliation
- Dispute resolution & fraud operartions
- Wealth management
- Compliance
Support Functions
- Legal
- Risk Management & Internal Audit
- IT
- Security Team
- Human Resources
- Finance Department
External Stakeholders
Supervisory Authorities
- ACPR (Primary Supervisor) & Banque de France
- European Central Bank
- CNIL (Data Protection)
- TRACFIN (Anti-Money Laundering)
- European Banking Authority (EBA)
Technical & payments Partners
- Mastercard (card scheme)
- Arkea (SEPA scheme + accounts management)
- SWIFT (non SEPA scheme)
Operations Partners
- Exceet (card production)
- Ubble (identity verification)
- Checkout (top ups)
Infra & tech Providers
- AWS (infra tech)
- Customer.io (marketing platform)
- Metabase (BI dashboards)
Service Providers
- Efficiale
- xxx (periodic control)
- ECAI (b)
Customers
- Physical persons
- Legal persons
System overview
Procedures
Procedures list
- Procedures to comply with national and international sanctions
- Blocking funds and economic resources of designated persons/entities
- Implementation of EU, UN, and national freezing measures
- Regular screening against sanctions lists
- System for detecting and reporting suspicious activities
- Alert generation based on predefined scenarios
- Alert investigation and escalation procedures
- Management of false positives
- Procedures for reporting suspicious transactions to TRACFIN
- Identification of suspicious patterns
- Documentation requirements for declarations
- Timeline and process for submission
- Follow-up procedures
Transaction Monitoring [en] [fr]
- Real-time and post-facto transaction surveillance
- Detection of unusual patterns
- Risk-based monitoring rules
- Threshold management
- Investigation procedures
PSEE Selection (Prestataires de Services Essentiels Externalisés) [en] [fr]
- Selection and evaluation of critical service providers
- Due diligence process
- Risk assessment of outsourcing
- Ongoing monitoring of service providers
- Contractual requirements
- Classification of incidents
- Response procedures
- Escalation matrix
- Resolution tracking
- Regulatory reporting requirements
- Post-incident analysis
KYB (Know Your Business) [en] [fr]
- Business customer due diligence
- Corporate structure verification
- Beneficial ownership identification
- Business activity assessment
- Risk scoring methodology
KYC (Know Your Customer) [en] [fr]
- Individual customer due diligence
- Identity verification
- Document validation
- Risk profiling
- Ongoing monitoring
- Periodic reviews
PUPA (Plan d’Urgence et de Poursuite d’Activité) [en] [fr]
- Business Continuity Planning
- Disaster Recovery Procedures
- Critical function identification
- Recovery time objectives
- Testing and maintenance protocols
ISSP (Information Systems Security Policy) [en] [fr]
- IT security framework
- Access control policies
- Data protection measures
- Security incident response
- System monitoring
- Security awareness training
- Risk identification
- Risk assessment methodology
- Risk mitigation strategies
- Risk monitoring and reporting
- Risk appetite framework
- Control effectiveness evaluation
- Segregation of client funds
- Protection of customer assets
- Account structure
- Reconciliation procedures
- Audit trail requirements
- Fraud prevention measures
- Detection systems
- Investigation procedures
- Recovery processes
- Reporting protocols
- Training and awareness
Authentication
Flow & architecture
Authentication — flow & architecture
How a customer proves who they are. This crate (src/authentication) owns three
things:
- the legacy email/password login (
POST /customer_auth/login, still used by web / business / back-office), - the mobile device-key flow — account creation and daily login without typing a password, and
- the per-request HTTP signature that makes a leaked session token useless on its own.
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 key | Device authentication key | |
|---|---|---|
| Unlock | device-unlock only, no per-use biometric | Face ID OR the app PIN (local) |
| Signs | every authenticated request (RFC 9421) | login / enrollment challenges |
| Public key stored | on the device row (the device crate) | as a DeviceKey credential (this crate) |
| Server holds a secret? | no — only the public key | no — 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:
User(usercrate) — the login principal. This crate never creates users; it attaches credentials and sessions to a user thatretail_apicreated.LoginIdentifier— links an email/phone to a user, separate fromUserIdso identifiers can change. Has a status (Unverified/Verified; KYC flips it).Credential— credential data viaCredentialEnvelope:Password { hash }orDeviceKey { public_key_spki, algorithm, device_id }(device authentication key, bound to the device it was enrolled on — a key only authorises logins from that device).Device(devicecrate) — browser or mobile device, created anonymously before login. Carries the request-signing key (signing_public_key_spki).Session— an active authentication context; oneValidsession per(user_id, device_id).AuthFlow/PendingChallenge— the server-driven flow state (below).AuthenticationAttempt— audit row for every attempt (success and failure).
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.
AuthFlow(domain::auth_flow) — one in-progress ceremony (login or enrollment) with its device, resolved user, staged identity/key, and satisfied factors.PendingChallenge(domain::pending_challenge) — a challenge for the current step; the client satisfies one to advance. Carries a single-usenonce.ChallengeFactor—DeviceKeyRegistration(bind the authentication key) andDeviceKey(prove it). Future factors (NFC, push, …) are new variants.Gate/gate_of(domain::gate) — the gate model. Factors map to gates; a future step-up must cross a gate boundary. V0 only exercises Gate 1 (Device).trust_matrix::next_step— the hardcoded V0 policy: given the flow + context it returns the next acceptable factor(s),Done, orReject. Adding a factor is a new match arm; the exhaustive match forces every site to handle it.
Endpoint surface (the flow lives in this crate; onboarding’s entry is in
retail_api):
| Endpoint | Service | Purpose |
|---|---|---|
POST /customer_auth/start | authentication | begin a login flow |
POST /customer_auth/respond | authentication | advance any flow (login or onboarding) |
POST /retail/onboarding/start | retail_api | create the user, begin enrollment |
POST /retail/device/create_device | retail_api | register a device, bind the request-signing key |
POST /customer_auth/login / POST /customer_auth/logout | authentication | legacy 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):
- Resolve
IdentifierKey→user_id. strategy.verify(user_id, credentials)(Argon2id forPasswordStrategy).- Resolve the device; reassign it to this user if it belonged to another (and revoke that user’s sessions on it).
- Supersede any existing
Validsession on the same device. - Create the session (lifetime by device type — see below).
- 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
- One
Validsession per(user_id, device_id)— a new login supersedes the prior session on the same device while the user’s other devices stay logged in (DB unique index on(user_id, device_id) WHERE status = 'Valid'). - Lifetime by device type (sliding): every valid request re-extends the
session to
now + windowinvalidate_session— web 5 minutes (PSD2 inactivity), mobile 30 days. Both use the samesession_duration(device_type)helper as session creation. Safe to extend the mobile window because every mobile request is Key-A-signed, so a leaked token is inert. - Logout (
revoke_session) revokes only the session row; the device’s request-signing key and the user’sDeviceKeypersist, so re-login on the same device stays single-factor. Idempotent.
Per-request signing (RFC 9421 + RFC 9530)
Two layered middlewares protect authenticated routes:
session_auth_middlewarevalidates the session token and injects theSession+DeviceId.request_signature_middleware— iff the device has a request-signing key — verifies anecdsa-p256-sha256HTTP Message Signature over the components@method @authority @path @query authorization(pluscontent-digestwhen the request has a body), withcreatedwithin ±5 minutes and a per-requestnoncechecked againstauth_request_noncefor 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)
SIGNING_DOMAIN_TAG=GG-AUTH-v1\n— domain separation and the protocol-format version (the single source of version truth).audience=signing_audience()→green-got/<environment>/auth, where<environment>is the logical env fromenv::STACK.env()—production/staging/development. Staging, sandbox and every PR-preview stack collapse tostaging(not the raw stack slug), so a staging build’s signature works unchanged against any preview. Recipient/environment binding (anti cross-context replay), on top of the per-environment single-use nonce.factor_byte=0x01registration /0x02device_key.- Challenge signatures are ASN.1 DER; per-request signatures are raw
r‖s. The verifier (aws-lc-rs) accepts either S parity, so no low-S normalization is required.
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
- Server-driven flow. The server issues challenges and decides the next step
(
trust_matrix); the client just fulfils options. The matrix is hardcoded in V0 (ADR-002) and reasons in gates, so future gate-independence rules drop in without reshaping. - Two keys, asymmetric. See above — biometric ergonomics + “DB can’t impersonate.”
- Per-device sessions. Re-keyed from
(user_id, device_type)to(user_id, device_id)so multiple phones / browsers coexist. - LoginIdentifier separation. Email can change without affecting
UserId; lookups use a normalizedIdentifierKey(lowercase email, trimmed phone). - Audit superset. Gate + SCA category recorded now to avoid a ClickHouse backfill later.
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.md— the mobile client contract.client_integration.md— web / password client headers.auth_middleware.md— using auth in other services.local_testing.md— local testing with seeds.
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 key | Device authentication key | |
|---|---|---|
| Unlock | device-unlock only, no per-use biometric | Face ID OR the app PIN (local) |
| Signs | every authenticated HTTP request (RFC 9421) | server-issued login / onboarding challenges |
| Registered | once, at device creation (POST /retail/device/create_device) | during the secure_device onboarding step |
| Server stores | public key on the device row | public key as a DeviceKey credential |
| Server holds any secret? | No — only the public key | No — 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.
- request-signing key: access control
kSecAccessControlPrivateKeyUsageonly (device unlock, no biometric prompt per signature). - device authentication key: add
.biometryCurrentSet/.userPresenceand allow device passcode fallback so the app PIN can release it.
Android (Keystore). KeyGenParameterSpec with
KeyProperties.KEY_ALGORITHM_EC, setDigests(DIGEST_SHA256),
setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")), and
setIsStrongBoxBacked(true) where available.
- request-signing key:
setUserAuthenticationRequired(false). - device authentication key:
setUserAuthenticationRequired(true)(and a validity duration / per-use auth as appropriate).
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)
| Thing | Encoding |
|---|---|
| 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 params | base64 (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
| Purpose | Request |
|---|---|
| Create an onboarding | POST /retail/onboarding/start — body { "offerId": "...", "country": "FR", "legalForm": "..." }, header x-device-id |
| Daily login | POST /customer_auth/start — body { "identifier": { "kind": "email", "value": "..." } }, headers x-client-type: mobile, x-device-id |
| Advance login flow | POST /customer_auth/respond — body below |
| Advance onboarding | POST /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/…); authstartandrespondlive 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
device_key— daily login. Unlock the device authentication key (Face ID or PIN, your choice), sign the challenge, send the signature.secure_deviceonboarding step — generate the device authentication key, sign the step’s raw challenge string, then submit the key’s public key and signature. Set up the local Face ID / PIN unlock on these screens — no backend involvement.
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
| Field | Value |
|---|---|
"GG-AUTH-v1\n" | the literal 11 bytes 47 47 2D 41 55 54 48 2D 76 31 0A (note the trailing \n) |
audience | the environment string green-got/<environment>/auth — green-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) |
0x00 | one separator byte |
factor_byte | 0x02 for device_key |
challengeId | UTF-8 bytes of challengeId (e.g. chl_…) verbatim |
nonce | raw bytes of base64-decode(params.nonceB64) |
deviceId | UTF-8 bytes of params.deviceId (e.g. dvc_…) verbatim |
identifier_hash | raw 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>:
| Component | Value |
|---|---|
@method | upper-case method, e.g. GET |
@authority | the Host header value |
@path | the 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) |
authorization | the full Authorization header value, e.g. Bearer eyJ… |
content-digest | the 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
- Store the session
tokenand device ID in the Keychain / Keystore (the token is a bearer; treat it as a secret, but remember it is inert without the request-signing key). - Send the token as
Authorization: Bearer <token>on authenticated requests (a cookie namedtokenis also accepted). Always pair it with the request-signing key signature. - Idempotent retry.
POST /customer_auth/respondis safe to retry: re-submitting an already-consumedchallengeIdreturns the current step result (the samechallenge_required/completepayload), never a spuriousfailed. So a lost response can be retried without restarting the flow. - Uniform errors. A refused flow always returns
{ "status": "failed", "error": "unauthenticated" }— identical whether the identifier is unknown, the device is unbound, the signature is wrong, or the challenge expired. Do not branch on the reason; just surface a generic “couldn’t sign you in” and let the user retry / contact support. 400 UnknownDeviceonPOST /retail/onboarding/start. Thex-device-idisn’t a registered device (never created, or cleared server-side — e.g. a DB reset). Register the device first (§5,POST /retail/device/create_device) and onboard with the fresh device ID; clear any stale cached id.401vs403— the two are distinct, and only one means re-login. Both carry the standard{ "error": "<Code>" }body (same envelope as every other API error), so branch on the code, not just the status:401 Unauthorized(also sendsWWW-Authenticate: Bearer error="invalid_token") — the session is gone. Start a new auth flow. This is the only re-authenticate signal. Codes:SessionExpired,SessionRevoked,SessionNotFound(e.g. the backend DB was reset),InvalidToken,MissingToken.403 Forbidden— the session is valid but the request signature was rejected. Re-sign and retry — do not re-login. Codes:SignatureMissing,SignatureInvalid,SignatureClockSkew(re-check the clock),SignatureNonceReused(mint a freshnonce). Rebuild the signature base and resend.
10. Platform fallbacks
- No biometric hardware / biometrics disabled — PIN-only: the device authentication key is unlocked by the app PIN. The flow is identical; only the local unlock UI changes.
- Android StrongBox absent — fall back to TEE-backed Keystore
(
setIsStrongBoxBacked(false)); the key is still hardware-isolated and non-extractable. The backend is agnostic. - Biometric re-enrollment invalidates the device authentication key (iOS
.biometryCurrentSet, AndroidsetInvalidatedByBiometricEnrollment). If signing the device authentication key fails with a key-invalidated error, fall back to the PIN-unlocked path (or re-enroll the device authentication key if it was biometric-only). The request-signing key is unaffected — it is not biometric-gated. - App reinstall / keychain cleared — the keys are gone. In V0 this means
registering a new device (
POST /retail/device/create_devicemints a new device ID) and onboarding again is the only path; silent re-binding of the request-signing key to an existing device is deliberately not allowed (it would let a DB-write attacker swap the signing key). Replacement-device enrollment on an existing account is V1.
11. End-to-end walkthroughs
A. First-launch onboarding
- Generate the request-signing key.
POST /retail/device/create_devicewith its public key → store the returned device ID. POST /retail/onboarding/start({ offerId, country, legalForm }, headerx-device-id) → response containsprospectTokenand the createdonboarding.- Submit onboarding steps through
POST /retail/onboarding/submit_step. When the next step issecure_device, first submit{ "stepCode": "SecureDevice" }to receive the challenge in the step prefill. - Generate the device authentication key, sign the challenge with it, then submit
{ "stepCode": "SecureDevice", "publicKeySpki": "...", "signature": "..." }. - Set up the local Face ID / PIN unlock for the device authentication key on these screens.
- 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)
POST /customer_auth/start({ identifier: { kind: "email", value } }, headersx-client-type: mobile,x-device-id) →challenge_requiredwith onedevice_keyoption.- Unlock the device authentication key — Face ID or PIN (user’s choice). Build the §7 payload
(
factor_byte = 0x02), sign it (DER). POST /customer_auth/respondwithsignatureB64→completewith a freshtoken.
C. A signed authenticated request
- Build the §8 signature base for the request (add
Content-Digestif there’s a body). - Sign with the request-signing key, raw
r‖s, base64. - Send the request with
Authorization,Signature-Input,Signature(andContent-Digestwhen there’s a body). Valid →200; bad/missing signature →403(re-sign & retry); expired/invalid session →401(start a new auth).
Quick reference
- Curve: ECDSA P-256 / SHA-256 for both keys.
- Public keys: SPKI DER → base64.
- Challenge signatures: DER → base64. Per-request signatures: raw
r‖s(64 bytes) → base64. Either S parity is accepted. - Challenge payload audience:
green-got/<environment>/auth(e.g.green-got/production/auth). Per-request@authority: the Host header. factor_byte:0x02for Customer Authdevice_key; Retailsecure_devicesigns the raw issued challenge string and has nofactor_byte.- Clock skew window: ±300s.
nonce: fresh per request, never reused. - Always send
x-device-id; sign every authenticated request once the request-signing key exists.
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:
| Header | Required | Description |
|---|---|---|
x-client-type | Yes | web or mobile |
User-Agent | Yes (web) | Sent automatically by browsers |
x-device-fingerprint | No (web) | Optional browser fingerprint, stored as metadata |
x-device-id | No (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
| Status | Body | Reason |
|---|---|---|
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) |
422 | — | Validation 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
- The server is running locally
- Migrations have been applied
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:
| Field | Value |
|---|---|
test@example.com | |
| Password | password123 |
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
- 1. Bills Overview — AP scope, the AR/AP boundary, and inbound channels (PA, email-forward, upload).
- 2. Received Invoice Domain — the Bill aggregate, its creation from
InboundInvoiceReceived, the AP lifecycle, the inbound status mapping (polling-authoritative), and the approval workflow with concrete MVP defaults (roles, thresholds, maker/checker, commands/events, delayed/scheduled/batch, support intervention). - 3. Payment and Matching — paying a bill on Green-Got’s rails, the four separate states (payment vs. acceptance), the
paid/Encaissée 212[open — provider-support]question, and auto-attaching the outgoing transaction.
Settled vs. open
- Settled: the supplier acceptance state (Accepted 205 / Refused 210) is the only confirmed PA-emittable buyer decision and is kept separate from the Green-Got bill-payment state; incoming originals are retained in core S3
bills/under the four-basis model (statutory 6-yr VAT / 10-yr accounting window → product archive promise; permanent only for non-PII integrity hashes — see PA — 13. Privacy §4), not “forever” as PII; AP matching emits no cross-crate event; thebil_id prefix. Approval MVP defaults are set in 2 §6.1 (revisable). - Open: whether a received French invoice can surface AFNOR Encaissée (212) via
mark_as paidis[open — provider-support](see 3 §1.1, §3). The full per-org approval-policy editor (custom roles, per-category thresholds, N>2) is deferred beyond the MVP defaults.
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:
- AP (Accounts Payable): invoices a Green-Got customer receives from their suppliers and owes payment on. Owned by the bills crate. The mirror of AR.
- AR (Accounts Receivable): invoices a Green-Got customer issues to their own clients. Owned by the invoicing crate, never by bills.
- Bill: an incoming supplier invoice; the aggregate root of the bills crate. The AP counterpart of
invoicing.invoice. - Supplier: the vendor master record (name, SIREN/SIRET, IBAN, payment terms, default category) that issues bills to the customer.
- Inbound (received): the direction of a supplier e-invoice flowing into the customer through the PA. The opposite of outbound (issued).
- Transmission: a plateforme_agreee record representing one regulated exchange through B2Brouter. For inbound, the transmission is the transport-layer counterpart of a
Bill. Owned by plateforme_agreee, referenced by bills. InboundInvoiceReceived: the eventbus event plateforme_agreee emits when a supplier e-invoice has been received and persisted. bills consumes it to create aBill.- Matching: linking a
Billto the outgoing (debit) bank transaction that pays it. The AP mirror of AR transaction matching. - Receiving pillar: the customer-facing capability “catch invoices other businesses send me, approve them, pay them, and keep them attached to the payment”.
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:
- Received supplier invoices — the
Billaggregate: supplier identity, amounts, VAT, due date, the original document, and the structured data extracted from Factur-X / UBL / CII. - Approval — the AP workflow that routes a received bill to the member(s) authorised to accept or refuse it (shared
ApprovalPolicy, see 2. Received Invoice Domain). - Payment — a simple, one-click way to pay an approved bill on Green-Got’s own rails (an outgoing transfer from the customer’s account). See 3. Payment and Matching.
- Matching — automatically attaching the resulting outgoing bank transaction back to the
Billit paid. See 3. Payment and Matching. - Supplier master — the vendor records bills are grouped under.
Out of scope:
- Issued (AR) invoices the customer sends to their own clients — owned by invoicing.
- PA transmission, Factur-X formatting, webhook ingestion, the authoritative legal lifecycle status — owned by plateforme_agreee.
- Employee expense reimbursements — owned by expenses. The boundary is firm: a bill is a B2B supplier invoice to the organisation; an expense is an employee out-of-pocket reimbursement. A PA inbound document always becomes a
Bill, never an expense. - Accounting journal export (FEC / Pennylane) — owned by accounting_export, which subscribes to bill events.
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.
| Dimension | invoicing.invoice (AR) | bills.Bill (AP) |
|---|---|---|
| Direction | Outbound — issued by the customer | Inbound — received by the customer |
| Cash flow | Money in (collection) | Money out (payment) |
| Counterparty | The customer’s client | The customer’s supplier |
| Settling transaction | Incoming (credit) bank transaction | Outgoing (debit) bank transaction |
| Owning crate | invoicing | bills |
| Numbering | Gap-free, owned by invoicing | None — the supplier owns the number |
| Legal status authored by | The 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:
| Channel | Source | Structured on arrival? |
|---|---|---|
| Plateforme Agréée | InboundInvoiceReceived from PA (Peppol / B2Brouter network / email-to-PA) | Yes — B2Brouter delivers extracted structured data; the Bill skips OCR. |
| Email-forward | A document forwarded to a per-organisation reception address held by bills | No — unstructured PDF; bills OCRs it. |
| Manual upload | A member uploads a PDF in the app | No — unstructured PDF; bills OCRs it. |
| API import | Programmatic 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.
- PA channel — dedupe on the B2Brouter invoice id. The PA channel is idempotent on the B2Brouter invoice id carried by the transmission (the same id the PA itself dedupes transmissions on). Re-delivery of an already-received transmission resolves to the existing
Bill; it never creates a second one. This is the authoritative contract the OCR channels mirror. - OCR channels (email-forward, manual upload) — dedupe on a per-organisation natural key. OCR channels have no B2Brouter invoice id, so bills computes a per-organisation dedup key normalised on (supplier identity +
supplier_invoice_number+issue_date+total_incl_vat) — where supplier identity is the resolvedsupplier_id(falling back to normalised SIREN, else normalised supplier name) andsupplier_invoice_number/amounts are whitespace- and case-normalised. A PDF content-hash (SHA-256 of the stored original bytes) is a secondary guard that catches byte-identical re-uploads even before OCR resolves the natural-key fields. - Idempotency guarantee at creation. Bill creation on every channel is idempotent within an organisation: a creation request whose dedup key (or, for PA, B2Brouter invoice id; or, as a secondary guard, PDF content-hash) matches an existing non-terminal-cancelled
Billdoes not create a newBill— it resolves to the existing one and records aduplicate_detectedaudit annotation on it (acting actor/channel + the colliding source reference) rather than silently dropping the input. The natural-key dedupe is best-effort before OCR completes (the key fields may not yet be extracted): a duplicate detected only after extraction is merged onto the earliestBill(the later one is voided asduplicate, never paid), and the merge is recorded in the AP audit trail. The dedup key is advisory, not a hard uniqueness constraint — a genuine second invoice that happens to collide (e.g. a corrected re-issue under the same number) surfaces for member review rather than being blocked.
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.
- Matching criteria. A PA-delivered transmission is matched to an existing OCR
Billwithin the same organisation using the OCR dedup key of §4.1 — (supplier identity +supplier_invoice_number+issue_date+total_incl_vat) — with supplier identity preferentially resolved by SIREN. A match requires supplier identity and invoice number and total to agree (issue_date is a tiebreaker). - Prefer linking over creating. On a match, bills links the PA transmission onto the existing OCR
Bill— it sets theBill’stransmission_idandsourcebecomes PA-backed — rather than creating a secondBill. This preserves the legal trail and the OCRBill’s history (approvals, payment state). No duplicate is created. On no match, the PA transmission creates a newBillas normal (§4 PA channel). - OCR
Billalready Accepted/Paid when the PA counterpart lands. Linking is non-destructive and never regresses the lifecycle: the existing approval and payment state are preserved. If the OCRBillis already Accepted, the now-linked transmission carries the Accepted (205) decision the buyer already made (subject to the PA emission rules); if already Paid (terminal), the transmission is linked for the legal trail and theBillstays Paid — reconciliation never re-opens or re-decides a terminal bill. A reconciliation that would conflict with a terminal state (e.g. a PA counterpart whose amount disagrees) surfaces for support/member review (see 2 §6.4), never silently overwrites. BillReconciledevent. A successful link emits an AP-internalBillReconciled { bill_id, transmission_id, prior_source }audit event (org-scoped, not cross-crate) so the piste d’audit fiable records that an OCRBillacquired its regulated counterpart. This reconciles the “all channels converge on the same lifecycle” claim above: convergence happens at reconciliation, on a singleBill.
4.3 Email-forward reception address contract
The email-forward channel ingests a document forwarded to a per-organisation reception address owned by bills:
- Address format / provisioning. Each organisation is provisioned a stable, unguessable per-org reception address of the form
bills+{org_token}@<bills-inbound-domain>(the{org_token}is an opaque, non-enumerable token mapped toorganisation_id, not a guessable org slug). The address is provisioned at organisation onboarding and shown in the bills admin UI; it can be rotated on demand (e.g. after a leak), which issues a new token and retires the old one (the old token bounces with an explanatory notice after a grace window). - Attachment handling. Each PDF/Factur-X attachment becomes one
Bill(subject to §4.1 dedup); the bytes are stored in the core S3 bucketbills/subpart as thedocument_ref(see 2 §2.1). Non-document attachments and inline images are ignored; an email with no usable attachment yields a parse-failure (below). Oversized attachments are rejected against the provider limit. - Sender allow-listing (anti-spoofing security requirement). Forwarding is not open relay: a mail is accepted only when it passes the provider’s inbound authentication (SPF/DKIM/DMARC alignment) and the sender is on the organisation’s configured allow-list (org member addresses by default; suppliers/forwarders added explicitly). Mail failing authentication or not on the allow-list is rejected or quarantined for review, never auto-ingested — a forged sender must not be able to inject a payable
Bill. - Parse-failure / bounce. When ingestion fails (auth failure, disallowed sender, no usable attachment, undecodable PDF), the message is recorded as a failed ingestion and the forwarding sender is bounced/notified with the reason; failures are visible in the bills admin UI for retry. Failures never silently disappear.
- Admin UI. The bills admin UI surfaces the reception address (with rotate action), the allow-list editor, and the recent-ingestion log (accepted, deduplicated, and failed/quarantined messages) for the piste d’audit fiable.
5. Document-Set Map
| Doc | Scope |
|---|---|
| 1. Bills Overview | (this doc) AP scope, the AR/AP boundary, inbound channels, relationship to the transport layer. |
| 2. Received Invoice Domain | The Bill aggregate, its fields, the AP lifecycle state machine, the AFNOR / B2Brouter status mapping for inbound, and the approval workflow. |
| 3. Payment and Matching | Paying 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
- 2. Received Invoice Domain — the
Billaggregate and AP lifecycle. - 3. Payment and Matching — paying and auto-attaching the outgoing transaction.
- Plateforme Agréée — 5. Lifecycle Statuses — the 3-layer status model and the INBOUND mapping.
- Plateforme Agréée — 10. Integration Contracts — the inbound event contract and the AR/AP boundary.
- Plateforme Agréée — b2brouter/5. Receiving Invoices — the B2Brouter inbound API.
- Plateforme Agréée — 1. Reform Overview — shared terminology and the reception obligation calendar.
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:
- PA — 1. Reform Overview §Sources — service-public.gouv.fr, economie.gouv.fr, impots.gouv.fr.
- PA — 5. Lifecycle Statuses §Sources — AFNOR XP Z12-012.
- PA — b2brouter/5. Receiving Invoices §Sources — developer.b2brouter.net.
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:
- Bill: the AP aggregate root — one received supplier invoice. The mirror of
invoicing.invoice, but never the same type. - Extracted fields: the structured supplier-invoice data (supplier identity, amounts, VAT, due date, lines) that B2Brouter parsed from the Factur-X / UBL / CII document and carried on
InboundInvoiceReceived. document_ref: a reference to the original supplier document (and B2Brouter’s extracted structured data) held by the plateforme_agreee transmission; carried on the event, not the bytes themselves.transmission_id: the id of the plateforme_agreee inbound transmission thisBillwas created from. TheBill’s back-reference to the transport/legal trail.- Accept / Refuse: the buyer’s business decision on a received bill. Drives B2Brouter
mark_as accepted | refused, which becomes the supplier-facing AFNOR legal status Approuvée (205) / Refusée (210). - ApprovalPolicy: the rule set deciding which member(s) must approve a bill’s content before it can be Accepted. Defined as an amount threshold + N approvers; shared with the expenses crate per the architecture. MVP defaults (roles, thresholds, maker/checker) are in §6.1.
- Four-eyes release: an optional second authorisation that gates the payment/transfer itself above a configurable threshold, independent of content approval. See §6 and 3. Payment and Matching §2.
- Dispute (En litige, 207): a non-terminal state where the bill is contested with the supplier before a final accept/refuse decision.
- AFNOR-aligned received decision status set: the canonical internal
decision_statusset for inbound bills — a 1:1 image of the AFNOR XP Z12-012 decision/arrival codes applicable to a received invoice plus the pre-decision arrival state. The payment-axis codes 211/212 are not part of this set (they project fromBillPayment.state). Defined canonically in PA — 5. Lifecycle Statuses §9; see §4.
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):
| Field | Meaning | Used by bills to… |
|---|---|---|
transmission_id | The PA inbound transmission id | Store as the Bill’s back-reference to the legal trail. |
organisation_id | The receiving customer’s organisation | Scope the Bill to the org; resolve approval routing. |
document_ref | Reference 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_fields | Structured supplier-invoice data (Factur-X/UBL/CII parsed) | Populate the Bill fields — supplier identity, amounts, VAT, due date, lines. |
received_at | When the document was received | Stamp 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.
- Poll interval. Hourly (the canonical inbound cadence). A B2Brouter webhook may accelerate an early poll but never substitutes for it and never creates a
Bill. - Bill-creation SLA. A polled-and-persisted inbound transmission yields a
Bill(event consumed,Billcreated) within minutes of the poll that discovered it; the worst-case visibility latency is therefore bounded by the poll interval plus event-handling time. - Retry / backoff.
InboundInvoiceReceivedhandling (rules/→create_bill_from_pa) retries with backoff on transient failure; the event is processed idempotently (§2 design rule), so a retried or re-delivered event resolves to the existingBill. - Nightly reconciliation backstop. A nightly reconciliation sweep re-lists transmissions and ensures every received-and-persisted transmission has a corresponding
Bill, repairing any gap from a missed event. Deduped on the B2Brouter invoice id. - Alerting. A transmission with no
Billafter the nightly sweep, or a poll/handler error budget breach, raises an operational alert — a silent gap means a customer never sees a legally received supplier invoice.
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:
- Lookup / dedup key. Within the organisation, resolve by SIREN first (the stable legal-entity key); fall back to a normalised supplier name + IBAN match when SIREN is absent. The resolution key is the same supplier-identity component used by the bill dedup key (§4.1 of doc 1).
- Auto-create vs manual review. A confident SIREN match auto-links the existing
Supplier. No match on a PA-delivered bill (SIREN present and well-formed) auto-creates aSupplierfrom the structured identity. A name-only match that is ambiguous (multiple candidates, or OCR low-confidence) leavessupplier_idunresolved and surfaces the bill for member review to pick/create the supplier — it never guesses. - Missing-SIREN fallback. When no SIREN is available (common on the OCR channels), the
Supplieris created/linked on the normalised name (+ IBAN) key withsiren = NULL; the bill is still created so it is never lost, but it is flagged for the member to confirm the supplier identity (SIREN is a mandatory mention for PA-delivered invoices — see §3.1). - Audit. Supplier auto-create, auto-link, and member-confirmed resolution are recorded in the AP audit trail (the §6.2 event stream), so the piste d’audit fiable records how each bill acquired its supplier.
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:
- Minimal fields to create a
Bill. ABillis created as soon as the original document is stored (document_ref) and the organisation is resolved — document and org are the only hard requirements. All extracted fields may initially be absent; theBilllands in Received and extraction proceeds asynchronously. ABillis never dropped for missing fields; an unparseable document yields aBillflagged for manual correction, not a discarded input. - PA-mandatory vs OCR-optional. Fields that are mandatory mentions for a PA-delivered (regulated) invoice — supplier SIREN, supplier name,
issue_date,total_incl_vat,vat_breakdown,supplier_invoice_number— are required before the bill can be Accepted (and before any cross-channel reconciliation key is complete, §4.2 of doc 1) but are optional at OCR creation time. On the OCR channels these are best-effort extractions; on the PA channel they arrive structured and present. - Failure path. OCR failure or low-confidence extraction does not block creation: the
Billis created in Received, the missing/low-confidence fields are flagged, and the bill surfaces for member correction. The approval gate (§6) cannot complete until the mandatory-for-Accept fields are present and confirmed. - Field-correction audit. Member (or support) corrections to extracted fields are recorded in the AP audit trail (§6.2 event stream) with before/after values — the structured data is a parsed projection, and every correction to it is part of the piste d’audit fiable.
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 group | Field | Notes |
|---|---|---|
| Identity | id | bil_-prefixed time_sortable_id — the canonical Bill id prefix (analogous to inv_ / quo_ / cli_). |
organisation_id | Owning customer org. | |
source | Channel: PlateformeAgreee | EmailForward | Upload | ApiImport. | |
transmission_id? | PA inbound transmission (present only for the PlateformeAgreee source). | |
| Supplier identity | supplier_id | FK to the Supplier master. |
supplier_name | Legal name (denormalised from the document). | |
supplier_siren | 9-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). | |
| Amounts | currency | ISO 4217. |
total_excl_vat | Net total, integer cents (i64). | |
total_vat | VAT total, integer cents. | |
total_incl_vat | Gross total, integer cents — the amount due. | |
| VAT | vat_breakdown[] | Per-rate lines: { category_code (UNCL5305: S/Z/E/AE/K/G/O), rate, taxable_base, vat_amount }. |
| Dates | issue_date | Supplier’s invoice date. |
due_date | Payment due date; drives payment scheduling. | |
| Document | supplier_invoice_number | The supplier’s own number (the supplier owns it; bills never renumbers). |
document_type_code | UNCL1001: 380 invoice, 381 credit note (avoir), 386 prepayment, etc. | |
document_ref | Reference to the original Factur-X / UBL / CII artefact, retained in the core S3 bucket bills/ subpart (§2.1). | |
| Lines | line_items[] | { description, quantity, unit_price_excl_vat, vat_category_code, vat_rate, line_total }. |
| Lifecycle | decision_status | The 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). |
| timestamps | created_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 minimums — 6 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:
- payment — the
BillPayment.statemoney-movement axis (Unpaid → … → PaymentSettled), a separate field, never folded intodecision_status(3 §1.1, §1.2); - extraction (OCR / structured parse) — a creation-time parse step;
- approval — an approval gate that is a precondition of
Accepted, not a status; - scheduling — a payment attribute (chosen rail + execution date, see 3. Payment and Matching);
- accounting export — an export flag set after the bill is fully paid (
BillPayment.state == PaymentSettled).
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:
- Received is the entry state for every channel. PA-delivered bills land here directly with structured data.
- Invalid maps to the B2Brouter received status
invalid(validation failure) and to AFNOR Rejetée (213) on the legal layer — a platform/format problem, not a business refusal. - Available (203) / TakenInCharge (204) are the pre-decision acknowledgement states the recipient (us) emits as it begins processing.
- Accepted (205) and Refused (210) are the buyer’s business decisions; Accepted is gated by the approval workflow. PartiallyApproved (206) is the partial-acceptance status; it is not directly payable in the MVP (only an Accepted bill is payable — see the full-payment-only rule below) and must resolve to Accepted (e.g. on a corrected supplier document / credit note), Refused, or Disputed before payment.
- Disputed (En litige, 207) is non-terminal and resolves to accept or refuse. Suspended (208) / Completed (209) form a suspend-resolve loop.
- Payment is a separate axis, not a
decision_statusvalue. Paying an Accepted bill advancesBillPayment.state(PaymentInitiated → PaymentSettled), driven by Green-Got’s own payment rails; it does not movedecision_status, which stays Accepted. The bill being fully paid isBillPayment.state == PaymentSettled. AFNOR Paiement transmis (211) / Encaissée (212) are the transmission/payment projection of that money-movement axis, not buyer-decision states on the bill; whether the supplier ever sees Encaissée (212) viamark_as paidis[open — provider-support]— see 3. Payment and Matching §1.1, §3.
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 status | AFNOR legal status | How it is driven |
|---|---|---|---|
| Received | received (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. |
| Invalid | invalid | Rejetée (213) | B2Brouter: validation issue on the received document. |
| Available | received | Mise à disposition (203) | Recipient PA (us): made available in-app. |
| TakenInCharge | mark_as† | Prise en charge (204) | Green-Got (buyer): acknowledged, processing started. |
| Accepted | accepted | Approuvée (205) | Green-Got (buyer): mark_as accepted after approval. |
| PartiallyApproved | mark_as† | Approuvée partiellement (206) | Green-Got (buyer): partial acceptance. |
| Disputed | mark_as† | En litige (207) | Green-Got (buyer): dispute raised. |
| Suspended | mark_as† | Suspendue (208) | Green-Got (buyer): suspended pending document. |
| Completed | mark_as† | Complétée (209) | Green-Got (buyer): document provided. |
| Refused | refused | Refusé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_status — BillPayment.state / transmission):
Source (BillPayment.state) | B2Brouter received status | AFNOR legal status | How it is driven |
|---|---|---|---|
PaymentInitiated | mark_as† | Paiement transmis (211) | Green-Got (payer): payment initiated on GG rails. Internal-only today (no confirmed native target). |
PaymentSettled | paid‡ | Encaissé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 == PaymentSettled → paid → 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:
- Projected as a legal status — only Accepted (
accepted→ 205) and Refused (refused→ 210) (and thepaid→ 212 leg if/when[open — provider-support]closes). These assert a real AFNOR legal lifecycle fact. - Surfaced as a non-legal annotation — the recommended buyer-side states that are operationally useful for the supplier to see but have no confirmed native
mark_as(e.g. Disputed (207), Suspended (208)). These may be sent as a B2Brouterannotatednote — a free-text remark that carries no status change and no legal meaning. The annotated payload is a structured-but-non-legal note:{ bill_id, internal_status (e.g. Disputed/Suspended), human_readable_reason, annotated_at }; it never encodes an AFNOR code in a legal field. - Internal-only — decision states with no supplier-facing value (e.g. TakenInCharge (204), PartiallyApproved (206), Completed (209)) are recorded on the
Billonly (indecision_status) and not emitted at all. The payment-axis projection Paiement transmis (211) (BillPayment.state == PaymentInitiated) is likewise internal-only until/unless a native target is confirmed — it lives onBillPayment.state, notdecision_status.
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
- Invariant: A
Billis AP-only. It is never foreign-keyed toinvoicing.invoice, never shares a table or Rust type with an issued invoice. - Invariant: The
Billreferences its PA transmission (transmission_id) when the source isPlateformeAgreee; for email/upload sources there is no transmission and no regulated legal status until reconciled. - Invariant — payment is gated on the decision axis but lives on its own axis. A
Billis payable only whendecision_status == Accepted(acceptance is itself approval-gated, §6); paying then advancesBillPayment.state(PaymentInitiated → PaymentSettled) on full payment only. PartiallyApproved is not payable; it must first resolve to Accepted. Payment does not changedecision_status(it stays Accepted). Partial / multi-tranche payment is deferred (post-MVP). - Invariant — terminality lives on the right axis. On the decision axis, Refused and Invalid are terminal
decision_statusvalues; a refused bill is not re-decided. On the payment axis,BillPayment.state == PaymentSettledis terminal (a settled full payment is not re-settled). A fully-paid bill is not terminal indecision_status— itsdecision_statusremains Accepted, leaving the accept/refuse path correctable. A correction comes as a new supplier document (e.g. a credit note, type 381), which is its ownBill. - Invariant: The supplier owns the invoice number; bills never assigns or mutates
supplier_invoice_number. (Gap-free numbering is an AR concern in invoicing, not an AP concern.) - Invariant: The supplier acceptance state (Accepted / Refused) reaches the supplier only via plateforme_agreee
mark_as; bills never holds the authoritative AFNOR status. Thepaid→ Encaissée (212) leg is[open — provider-support](§4.1) and emits nothing until confirmed. - Invariant — payment state ≠ acceptance state. The Green-Got bill-payment state is distinct from the supplier acceptance/refusal state; a payment advances the former only and never implies a PA acceptance status. The two states are owned and modelled in 3. Payment and Matching §1.1.
- Invariant — AP matching emits no PA-facing paid signal. Attaching the settling outgoing transaction to a
Billand advancingBillPayment.statetoPaymentSettledis AP-internal: a completed transfer moves the AP payment state only (neverdecision_status) and triggers no PA-facing paid signal and no cross-crate paid event today — there is nomark_as paidon the received French channel (decision #14; thepaid→ Encaissée leg is[open — provider-support], §4.1). The buyer has no Flux 10 payment-data e-reporting obligation on an inbound payment — that obligation is the supplier’s. This contrasts with AR matching, which does emitTransactionMatchedbecause the seller carries the Flux 10 obligation. The separate AP acquisition-data event (ReportableAcquisitionRecorded, §3 §3.1) is not a paid signal and does not contradict this. See 3. Payment and Matching §4.2. - Invariant — the original artefact is retained for the statutory minimum. The incoming original (
document_ref) is stored in the core S3 bucketbills/subpart and its full PII-bearing bytes are kept for the applicable statutory retention — 6 years (VAT) / 10 years (accounting/commercial) under the piste d’audit fiable. Retention beyond that minimum is the Green-Got product archive promise (a separate purpose, lawful basis, and controls), and only non-PII hashes / audit evidence are kept permanently — the full original bytes are not held indefinitely under a legal-obligation basis (see PA — 8. Archiving and Audit §2).
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.
- Who approves: organisation members holding an approver role. The routing is driven by
ApprovalPolicy. - Policy (
ApprovalPolicy): an amount threshold + N approvers rule set, resolved against theBill’stotal_incl_vat(and optionally category). - Shared with expenses: per the architecture,
ApprovalPolicyis shared with theexpenses/crate (both are “an organisation member approves a spend”). The boundary stays firm — a PA inbound document is always aBill, never an expense — but the approval machinery is common. - Outcome: an approved bill can be moved to Accepted (
mark_as accepted); a rejected one is Refused (mark_as refused).
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.
- Approver roles (MVP). Two roles gate a bill: Approver (may approve a bill’s content → Accept) and Releaser (may authorise the payment/transfer — the four-eyes second signature). For the MVP these map onto existing org roles: Owner and Admin are both Approver and Releaser; Member is neither. No new role type is introduced for the MVP.
- Thresholds (MVP defaults, all in
total_incl_vat, per-bill). Thresholds are evaluated in EUR; the MVP is EUR-only (the billcurrencyisEUR). A non-EUR bill is out of MVP scope; when multi-currency lands, the threshold is compared against an FX-convertedtotal_incl_vatsnapshot taken at evaluation time, but no FX rule ships in the MVP — the MVP simply assumes EUR. §6.1 is the authoritative four-eyes threshold definition (the>€10,000rule below); 3. Payment and Matching §2 defers to it and does not redefine it.- Auto-route, single approval — under €1,000: one Approver approves; the bill becomes Accepted. No second signature.
- Two approvers (maker/checker on content) — €1,000–€10,000: two distinct Approvers must approve before Accept.
- Two approvers + four-eyes release — over €10,000: content needs two distinct Approvers; the payment additionally needs a Releaser distinct from both content approvers before the transfer is released.
- Maker/checker rule (MVP). The member who created/ingested or last edited a bill may not be the sole approver of it; at least one approval must come from a different member. Above €10,000 the Releaser must differ from every content approver (true four-eyes on the money). These distinctness checks are invariants, enforced server-side, not UI hints.
- Default rail is unchanged. Approval defaults do not change the payment rail; instant-by-default and the scoring/risk downgrade live in 3. Payment and Matching §2.
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) | Precondition | Effect | Audit event (AP-internal) |
|---|---|---|---|
SubmitForApproval | TakenInCharge | Routes the bill per ApprovalPolicy; records required approver count. | BillSubmittedForApproval |
ApproveBill | pending approval; approver ≠ maker | Records one approval; when the policy’s N is met → Accepted + mark_as accepted. | BillApproved, then BillAccepted |
RefuseBill | pending approval or Disputed | → Refused + mark_as refused. | BillRefused |
DisputeBill | TakenInCharge / Accepted | → Disputed. | BillDisputed |
ReleasePayment | Accepted; 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:
- On
RefuseBill. The prior approval records are preserved as audit history, not deleted —BillRefusedcaptures who refused and when, alongside the retained earlierBillApprovedrecords. ARefusedbill is terminal (§5), so the preserved approvals document the decision trail; they are never re-used to silently re-accept. The refusal must be captured in the AP audit trail (the §6.2 event stream) and in the ComplianceEventLog so the legal trail records the buyer’s decision. - On
DisputeBill. A dispute is non-terminal and may resolve back to Accept. Prior content approvals are preserved across the dispute for every actor (member or support): moving toDisputeddoes not void recorded approvals. On resolution to Accept, the policy’s required approver count must still be satisfied by the (preserved or newly added) approvals; on resolution to Refuse, the preserved approvals remain as audit history. Support raising a dispute (§6.4) never clears the customer’s approvals.
6.3 Delayed, scheduled, and batch behaviour (MVP defaults)
- Immediate vs scheduled. By default an Accepted (and, where required, released) bill is paid immediately on the chosen rail. The member may instead schedule payment for a future date (MVP default suggestion:
due_date); scheduling is a payment attribute — the rail and execution semantics are owned by 3. Payment and Matching §2. - Batch approval / batch pay (MVP). Approval and payment may be applied to a selection of bills in one action, but each bill is evaluated individually against its own
ApprovalPolicy, thresholds, and four-eyes/distinctness rules — a batch is a convenience loop, never a way to bypass a per-bill gate. A bill in the batch that fails its gate is skipped and surfaced, not silently dropped.- Per-bill atomicity. Each bill in the batch succeeds or fails independently; one bill failing its gate never rolls back or blocks the others.
- Concurrency. Bills in a batch may be processed in parallel up to a fixed concurrency cap (a small bounded fan-out, not unbounded) to protect downstream rails (
core_banking) and OCR/notification limits; ordering across bills is not significant since each is independent. - Structured result. A batch action returns a structured per-bill result — a list of
{ bill_id, outcome: success | failed, reason? }— so the caller sees exactly which bills succeeded and why each failure occurred (gate not met, distinctness violation, VoP/scoring block on pay, etc.). No aggregate-only success flag. - Retry the failed subset. Because the result is per-bill, the member can re-submit only the failed subset after correcting the cause (e.g. adding the second approver). Re-submission is idempotent against already-succeeded bills (a bill already Accepted/paid is a no-op success in a later batch).
- No auto-accept, no auto-pay (MVP). The MVP never auto-approves or auto-pays on a timer; every Accept and every payment originates from an explicit member command. (Auto-pay policies are deferred.)
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 cannot approve, accept, or release payment on a customer’s behalf — those decisions stay with org Approvers/Releasers (the maker/checker and four-eyes invariants hold regardless of actor).
- Support may act only from the allowed from-states below, and each support action has the listed guard:
| Support action | Allowed from-states | Guard |
|---|---|---|
| Re-trigger inbound polling / re-fetch the transmission | any non-terminal state | no state change; idempotent (§2 dedupe) |
| Re-run extraction | Received, Available, TakenInCharge, Disputed, Suspended | field-correction audit (§2.4); never on a terminal bill |
Move to Disputed | TakenInCharge, Accepted, PartiallyApproved | preserves approval state (§6.2); not from Refused/Invalid/Paid |
Move to Suspended | TakenInCharge | resolves via Completed (§4) |
| Re-link a mis-attached transaction | bill not in a terminal state (Refused/Invalid/Paid) | see terminal-state guard below |
- Terminal-state guard on re-linking. Re-linking (or unlinking) the settling transaction is forbidden once the bill is terminal —
Paid,Refused, andInvalidare terminal (§5) and their transaction attachment is part of the closed legal/payment trail. A genuine error on a terminal bill is corrected by an explicit, audited correction event, never by silently re-pointing the attachment. Support may never re-link in a way that would regress a terminal state or re-open settlement. - Every support action is recorded with the acting support identity in the AP audit trail (the same event stream as §6.2), so the piste d’audit fiable distinguishes customer decisions from support interventions. Support interventions preserve existing approval state for all actors (§6.2).
7. Related Documents
- 1. Bills Overview — AP scope, the AR/AP boundary, inbound channels.
- 3. Payment and Matching — paying a bill and auto-attaching the outgoing transaction.
- Plateforme Agréée — 5. Lifecycle Statuses — the canonical 3-layer status model and INBOUND mapping.
- Plateforme Agréée — 10. Integration Contracts — the
InboundInvoiceReceivedevent contract. - Plateforme Agréée — b2brouter/5. Receiving Invoices — the B2Brouter inbound API.
- Plateforme Agréée — b2brouter/6. Statuses and Webhooks — the
mark_asendpoint and status sets.
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:
- One-click pay: initiating payment of a
Billfrom a single action in the app — the customer-facing “simple way of paying them”. - Payment rail: the mechanism that moves the money — for Green-Got, an outgoing transfer from the customer’s own account executed via core_banking, on one of three rails the customer chooses: instant payment (SEPA Instant), classic SEPA, or scheduled SEPA. B2Brouter does not pay.
- Scoring / risk engine: the Green-Got engine that runs the mandatory payment controls (§2.1) — it may restrict the chosen rail (e.g. forbid instant payment), require an additional confirming document, route a transfer to maker-checker, or block it before a transfer is released.
- Outgoing (debit) transaction: the bank transaction that leaves the customer’s account when a bill is paid. The thing that gets attached to the
Bill. - Auto-attach / matching: linking the outgoing transaction to the
Billit settled, automatically, by amount / reference / beneficiary. - Verification of Payee (VoP): the EU Instant Payments Regulation name/IBAN check run before an outgoing credit transfer is released; it returns match | close match | no match | other (see §2.1).
- Maker-checker (four-eyes release): a second, independent authorisation of the transfer by a different member, mandatory above the configured amount threshold (§2.1, §2.2).
mark_as paid(received): the B2Brouter call (POST /invoices/{id}/mark_as,state: paid) that would record payment on an inbound transmission and might yield AFNOR Encaissée (212). For received French invoices this is not confirmed as a legally valid target state and is held[open — provider support](§3); B2Brouter’s genericmark_asreference listsnew | paid | accepted | refused | annotated, but the DGFiP receiving flow only confirmsaccepted/refusedfor the buyer.
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.
| # | State | Owner / store | Values | What changes it | Legally PA-emittable? |
|---|---|---|---|---|---|
| 1 | Green-Got bill-payment state | bills (BillPayment) | Unpaid → PaymentScheduled → PaymentInitiated → PaymentSettled (or PaymentFailed / PaymentReturned) | The outgoing transfer lifecycle on core_banking (§2.3). | No — purely internal money movement. |
| 2 | Supplier-invoice acceptance/decision state | bills (Bill.decision_status) → plateforme_agreee transmission | The AFNOR decision/arrival set — `Received | Available | TakenInCharge |
| 3 | B2Brouter local received state | B2Brouter (via plateforme_agreee) | new, accepted, refused, paid†, annotated | mark_as calls routed through plateforme_agreee. | Transport-layer; mirrors (2). |
| 4 | PPF / AFNOR legal status | DGFiP PPF (the authoritative legal layer) | the AFNOR XP Z12-012 code surfaced to the supplier | Derived 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 group | Field | Notes |
|---|---|---|
| Identity | id | bpay_-prefixed time_sortable_id. |
bill_id | FK to the Bill this payment settles. | |
organisation_id | Owning customer org (denormalised for scoping). | |
| Transfer | transfer_id | core_banking credit-transfer id this BillPayment initiated. |
rail | InstantSepa | ClassicSepa | ScheduledSepa (§2). | |
amount_cents | Settled amount = the bill’s total_incl_vat (full-payment-only). | |
currency | ISO 4217 (EUR in the MVP). | |
creditor_iban | Supplier IBAN paid (snapshot of supplier_iban at release). | |
execution_date? | For scheduled SEPA: the chosen future execution date. | |
| State | state | Unpaid → PaymentScheduled → PaymentInitiated → PaymentSettled, or PaymentFailed / PaymentReturned (§1.1, §2.3). |
| Four-eyes | released_by? | The Releaser who authorised the transfer (above the §6.1 threshold or on a VoP override). |
| VoP audit | vop_result | match | close_match | no_match | other (§2.1). |
vop_matched_name? | Name returned by the beneficiary bank. | |
vop_evaluated_at | When 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). | |
| Timestamps | timestamps | created_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:
| Rail | When it executes | Typical use |
|---|---|---|
| Instant payment (SEPA Instant Credit Transfer) | Within seconds, any time | The default — pay the supplier immediately |
| Classic SEPA (SEPA Credit Transfer) | Next business day | Fallback when instant is disabled, or no urgency |
| Scheduled SEPA | On 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).
- One-click pay from a Bill: an Accepted bill (approval already passed — see 2. Received Invoice Domain §6) exposes a single pay action; instant is pre-selected.
- The payment is the customer’s money leaving their account — the opposite cash-flow direction from AR collection. There is no payment processor and no B2Brouter settlement in the loop.
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:
- disable instant payment for a given bill or customer — e.g. an amount above a threshold, a suspect / unrecognised counterparty, or a high-risk internal customer — falling back to classic or scheduled SEPA; and
- require an additional confirming document (e.g. proof of the supplier relationship or the underlying order) before the transfer is released.
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 outcome | Meaning | Default effect on release |
|---|---|---|
match | Beneficiary name matches the account holder. | Pass — release may proceed (subject to scoring + four-eyes). |
close_match | Near 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_match | Name 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). |
other | VoP 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:
- Below the four-eyes threshold, a clean
matchreleases without a second signature; but aclose_match/no_matchoverride always pulls in a second member (the overriding Releaser must differ from the maker), so a VoP override imposes four-eyes regardless of amount. - Above the four-eyes threshold, the Releaser distinct from the content approvers (§6.1) must both authorise the transfer and clear/override any non-
matchVoP outcome — a single act when the same Releaser does both, but the maker ≠ Releaser and Releaser ≠ content-approver distinctness invariants both hold.
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:
- Format. A required category (a small closed enum — e.g.
LegalFormDifference,KnownSupplierAlias,TypoConfirmedWithSupplier,Other) plus a free-text justification. The free text is mandatory (an override without a reason is rejected server-side, as above) and is length-bounded (a reasonable cap, e.g. ≤ 1000 characters) to keep it an explanation, not a document dump. - PII handling. The free text may incidentally contain PII (a contact name, a phone confirmation). It is treated as bill-associated PII: stored alongside the
BillPayment, covered by the same access controls as the bill, and retained under the same statutory horizon as the payment audit trail; it is never exposed outside the organisation and Green-Got audit. - Immutability. Once written, the override category, reason, overriding actor, and timestamp are immutable — corrections are made by an appended audit note, never by mutating the original (the piste d’audit fiable must show what was actually decided at release time).
- Customer visibility. The override reason is visible within the organisation (to Approvers/Releasers and in the bill’s audit view) and to Green-Got audit/support; it is part of the internal control trail. It is not surfaced to the supplier and is not part of any PA/AFNOR emission.
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:
| Transition | Trigger | Notes |
|---|---|---|
Unpaid → PaymentScheduled | A 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 → PaymentInitiated | core_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 → PaymentSettled | core_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 / PaymentReturned | core_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). |
- Pre-initiation gates are atomic with persistence. VoP evaluation and any required four-eyes/override complete before
core_bankingis called. If a gate fails, no transfer is initiated and theBillPaymentis left in a non-released state (or not persisted as released) — there is no window whereBillPayment.stateis marked initiated/settled without a real transfer. - Rollback on VoP / core_banking failure. If VoP blocks (unresolved
no_match/other) orcore_bankingrejects the initiation request, theBillPaymentdoes not advance toPaymentInitiated(andBill.decision_statusis unaffected — it never left Accepted) — the attempt is recorded (with the VoP result/reason) and surfaced for resolution, but no state is optimistically advanced and then reverted.BillPayment.stateonly ever moves toPaymentInitiatedon acore_banking-accepted transfer, and toPaymentSettledon acore_banking-confirmed settlement. - Auto-attach timing. Auto-attach (§4) runs when the settling outgoing transaction is observed — for instant SEPA, effectively at settlement confirmation; for classic/scheduled SEPA, when the debit transaction is ingested from
core_banking. The transition toPaymentSettledand the attachment are driven by the same settlement observation, so aBillPaymentis neverPaymentSettledwithout (or before) its settling transaction is attached. A re-attempt afterPaymentFailed/PaymentReturnedre-runs the full gate → initiate → settle → attach sequence on a newBillPayment.
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:
- bills records the bill’s
BillPayment.stateasPaymentSettled(the AP payment state only;decision_statusis untouched). It emits no PA-facing paid signal — there is no unconditional payment signal to plateforme_agreee. - 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 B2BrouterPOST /invoices/{id}/mark_aswith{ "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 onlyaccepted/refusedas validmark_astargets 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:
- Payment data (buyer-side) — never emitted by AP. Settling a supplier
Billcarries no buyer-side payment-data obligation: the buyer has no seller-side collection to report. AP matching/payment therefore emits no payment-data event (noPaymentCollected-equivalent).PaymentCollectedis seller-side only — it belongs to AR (invoicing), where the customer is the one being paid and owes the Flux 10 payment-data report. AP never emits it. - Acquisition data (transaction-level) — owned by AP when in legal e-reporting scope. Separately, when a foreign-supplier bill is in legal e-reporting scope — a cross-border B2B acquisition under CGI art. 290 (the customer buys from a supplier outside France, so no domestic e-invoice carries the transaction to the DGFiP) — the transaction itself is reportable. In that case bills emits the canonical
ReportableAcquisitionRecordedAP acquisition-data event (producer = bills, consumer = plateforme_agreee), describing the acquisition. Its payload carries:bill_id,organisation_id, supplier identity (name + country + VAT / local registration id), operation type, taxable amounts, VAT treatment, invoice date, reporting period, source document ref, and correction/refusal state. This is the AP acquisition-data event and is distinct fromPaymentCollectedand from any paid signalling: it reports the acquisition (not a payment received, not a payment made), and it fires off the bill (its cross-border scope), not off the outgoing transfer. The PA-side event table, storage, and wiring are owned by plateforme_agreee; bills is the producer side described here.
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
| Criterion | How it matches |
|---|---|
| Amount | The debit amount equals the bill’s total_incl_vat (same currency). |
| Reference | The transfer’s remittance reference (derived from supplier_invoice_number) appears on the transaction. |
| Beneficiary | The 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):
- The deterministic id-carried match always wins outright — if the transaction carries a bill id, no fallback is consulted.
- Otherwise the tiebreaker order is: (1) exact reference match (normalised) beats amount+beneficiary; (2) among equal-strength candidates, the closest
due_date/issue_dateto the transaction date; (3) if still ambiguous, no auto-attach — the match is proposed for member confirmation (MatchProposed), never auto-applied. A debit is never attached to more than oneBill, and aBill’s settling attachment is never silently re-pointed.
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 direction | Outgoing (debit — money out) | Incoming (credit — money in) |
| What is settled | A supplier Bill (we owe) | An issued invoicing.invoice (we are owed) |
| Who initiates the transfer | Green-Got (the payer) | The customer’s client (the payer) |
| Legal-status effect | None 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 event | none cross-crate required (AP-internal) — ReportableAcquisitionRecorded is a separate cross-border acquisition event (§3.1), not a paid signal | TransactionMatched → plateforme_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
- Invariant: Payment is an outgoing transfer from the customer’s account via core_banking; B2Brouter never moves money.
- Invariant: Only an Accepted bill is payable.
- Invariant — payment axis is distinct from the decision axis. Advancing
BillPayment.statetoPaymentSettledrecords the AP-internal payment state only; it does not changeBill.decision_status(which stays Accepted) and does not by itself emit a PA legal status. The supplier-facing AFNOR Encaissée (mark_as paidon the received invoice) is gated[open — provider-support]until B2Brouter confirms it for French received invoices, and — if confirmed — is emitted exclusively through plateforme_agreee; bills never calls B2Brouter directly. Neither Paiement transmis (211) nor Encaissée (212) is aBill.decision_statusvalue. - Invariant: Auto-attach links the outgoing (debit) transaction to the
Bill— the opposite direction from AR matching’s incoming credit. - Invariant — full-payment-only (MVP): a
Bill’sBillPayment.statereaches terminalPaymentSettledvia a single full settling transfer; it is never split across tranches. MultipleBillPayments may exist only as re-attempts after aPaymentFailed/PaymentReturned. Partial / multi-tranche payment is deferred (post-MVP) and not modelled here. - Invariant — AP matching emits no payment-data event and no PA-facing paid signal. Auto-attaching the outgoing transaction and advancing
BillPayment.statetoPaymentSettledis AP-internal: a completed transfer moves the AP payment state only (neverdecision_status) and triggers no payment-data eventbus event, no cross-crate paid event, and no PA-facing paid signal (decision #14 — there is nomark_as paidon the received French channel today). The buyer has no Flux 10 payment-data e-reporting obligation on an inbound payment (that is the supplier’s obligation). If a future staging run confirms receivedpaid, it becomes a distinct gated event, never a generic unconditional signal (§3,[open — provider-support]). This is the deliberate contrast with AR matching, which does emitTransactionMatchedbecause the seller carries the Flux 10 obligation (§4.2). The separate acquisition-data eventReportableAcquisitionRecordedon an in-scope cross-border foreign-supplier bill (§3.1) is not a payment-data event and not a paid signal, and does not contradict this: it is produced by bills, fires off the bill’s cross-border scope (not off payment/matching), and is consumed by plateforme_agreee. - Invariant — mandatory pre-release VoP gate. No outgoing transfer is initiated until VoP (§2.1) has been evaluated and recorded on the
BillPayment. Ano_match(or fail-closedother) blocks the instant rail; releasing on any rail after a non-matchoutcome requires a Releaser override (the maker may never override their own payment) with a captured reason, and the VoP result, matched name, timestamp, and (where applicable) overriding actor and reason are recorded immutably.
6. Related Documents
- 2. Received Invoice Domain — the
Billaggregate, AP lifecycle, accept/refuse, and approval. - 1. Bills Overview — AP scope and the AR/AP boundary.
- invoicing — 9. Transaction Matching — the AR (incoming/credit) matching counterpart, for contrast.
- Plateforme Agréée — 5. Lifecycle Statuses — the INBOUND status mapping (Encaissée).
- Plateforme Agréée — 7. E-Reporting — Flux 10 payment-data reporting (an AR / supplier concern, not the buyer’s).
- Plateforme Agréée — 10. Integration Contracts §8 — payment collection and the no-settlement rule.
- Plateforme Agréée — b2brouter/6. Statuses and Webhooks — the
mark_asendpoint.
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
- 1. Invoicing Overview — AR scope and the split with the transport layer.
- 2. Dual Invoice Model — the classical internal model (UI/editing) and the EN 16931 structured model, and how one derives the other.
- 3. Invoice Domain — the Invoice aggregate, its canonical state machine, reflected legal status, and credit notes (avoir / type 381).
Documents & numbering
- 4. Quote Domain — the devis: create / send / remind / sign (elsewhere) / archive, and conversion to invoice.
- 5. Numbering — gap-free sequential numbering per legal entity, annual reset, credit-note series.
- 6. Quote Signature — the e-signature contract invoicing depends on (the provider implementation lives in a separate crate).
Lifecycle, money & reconciliation
- 7. Reminders — manual and automatic payment/validity reminders.
- 8. Payment Link and Collection — letting the customer’s client pay (Green-Got rails — B2Brouter does no payments).
- 9. Transaction Matching — linking an issued invoice to the incoming bank transaction; triggers Encaissée + Flux 10.
- 10. Tax and VAT — VAT rates, EN 16931 category codes, line-level chargeability (debits vs collection) with document-level derivation, totals, and the Article 290 operation taxonomy tie-in.
- 11. API Contract — the domain-to-
business_apicontract: draft vs finalized payloads, typed SIREN/SIRET/VAT/address fields, per-line tax + BT-21 legal notes, immutable finalization fields, status projection, and the generated-SDK migration path.
Other
- Uncertainties — the active, classified register of open AR items (BT-21 codes, identifier length rule, DTO migration), each with owner, milestone, launch-blocking flag, and evidence link.
To Be Created
- Recurring invoices (schedule, auto-send) — document when scoped.
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:
- AR (Accounts Receivable): documents a Green-Got customer issues to their own clients — quotes and invoices. Owned by the
invoicingcrate. The customer is the seller; their client is the buyer. - AP (Accounts Payable): documents a Green-Got customer receives from their suppliers. Owned by the
billscrate. Explicitly out of scope forinvoicing— see §4. - Issued invoice: a finalized AR commercial invoice (document type 380) that the customer sends to a buyer. The AR record from which the EN 16931 structured invoice is generated for transmission.
- Quote (devis): a pre-sale offer that, once accepted/signed, may be converted into an invoice. Not an e-invoice itself; never transmitted over Flux 1.
- Avoir (credit note): a corrective AR document (document type 381) that reverses or adjusts a finalized invoice. Finalized invoices are immutable, so corrections issue an avoir — see 3. Invoice Domain.
- Classical internal model: the human-facing representation of an invoice/quote used for authoring, display, editing, and branding.
- EN 16931 structured model: the machine-readable representation generated from the classical model at finalize time; the transmission source of truth. Defined canonically in PA 4. Formats and Invoice Data.
- Transmission: a
plateforme_agreeerecord for one regulated exchange through B2Brouter. The transport-layer counterpart of an AR invoice. Owned byplateforme_agreee, not byinvoicing. - Legal lifecycle status: the AFNOR XP Z12-012 status set. Owned authoritatively by
plateforme_agreee;invoicingonly holds a projection of it. - Projection: a read-model copy of a fact (here, the legal status), kept current by eventbus events, never the authoritative record.
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:
- Quotes — create, send, remind, archive; signature is captured through a shared e-signature provider (see 6. Quote Signature). Quote authoring and lifecycle live in 4. Quote Domain.
- Invoices — author (classical model), finalize (assign a gap-free number, lock the document, generate the EN 16931 structured model), and project the transmission’s legal lifecycle status. See 3. Invoice Domain.
- Credit notes (avoir, type 381) — the only compliant way to correct a finalized, gap-free invoice. See 3. Invoice Domain.
- Gap-free numbering — sequential, gap-free per-issuer document numbers assigned at finalize time. See 5. Numbering.
- Clients — the AR party master (the customer’s buyers): identity (company/individual), billing and delivery addresses, contacts, default document language/currency/payment terms.
- Payment links and collection — Green-Got generates payment links on its own rails. B2Brouter performs no settlement. See 8. Payment Link and Collection.
- Reminders — manual and automated reminder cadences for unpaid invoices. See 7. Reminders.
- AR ↔ bank-transaction matching — reconciling incoming bank transactions to issued invoices. A full-amount match (
TransactionMatched) drives the ARPaidstatus; a real bank-level collection (PaymentCollected) writes the VAT-collection ledger that is the internal data source for payment-data e-reporting (carried asEnriched212Menfor invoiced Flux-1 operations,Flux10only for non-invoiced). See 9. Transaction Matching. - Tax / VAT — VAT category codes, rates, multi-currency, and the VAT-chargeability option (debits vs encaissement). See 10. Tax and VAT.
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.
| Concern | invoicing (AR) | plateforme_agreee (transport) |
|---|---|---|
| Authoring the classical invoice + generating the EN 16931 structured model | Owns | — |
| Gap-free numbering, finalization, immutability | Owns | — |
| Credit notes (avoir) | Owns | — |
| The regulated transmission record | — | Owns |
| Mapping EN 16931 → B2Brouter JSON; B2Brouter HTTP adapter | — | Owns |
| Webhook ingestion + HMAC verification; CDAR | — | Owns |
| The authoritative AFNOR legal lifecycle status | Projects only | Owns |
| 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.
| Event | Direction | Purpose |
|---|---|---|
InvoiceFinalized | invoicing → plateforme_agreee | A 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. |
TransmissionStatusChanged | plateforme_agreee → invoicing | An authoritative AFNOR legal status changed. Carries transmission_id, invoice_id, legal_status, changed_at, optional reason. invoicing updates its projection. |
TransactionMatched | invoicing → plateforme_agreee | A 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. |
PaymentCollected | invoicing → plateforme_agreee | A 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. |
InboundInvoiceReceived | plateforme_agreee → bills | A 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_agreee → invoicing. 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:
- Receiving supplier invoices (AP). Owned by
bills. The inbound document is transported byplateforme_agreeeand handed tobillsviaInboundInvoiceReceived; it is never FK’d toinvoicing.invoice. See bills/1. Bills Overview and bills/2. Received Invoice Domain. - The regulated transmission itself. Owned by
plateforme_agreee. - The authoritative legal status, routing, format generation (Factur-X/UBL/CII), and e-reporting transmission. All owned by
plateforme_agreee/ B2Brouter. - The B2Brouter account / mandate / onboarding. Organisation-level; owned by
plateforme_agreeeandorganisation. See PA 9. Mandate and Onboarding. - Payment settlement by the PA. B2Brouter settles nothing; collection is Green-Got’s own rails (see 8. Payment Link and Collection).
5. The Invoicing Doc Set
| Doc | Topic |
|---|---|
| 1. Invoicing Overview | This document — AR scope, relationship to the transport layer. |
| 2. Dual Invoice Model | The two coupled representations: classical internal model + EN 16931 structured model, and how one derives the other. |
| 3. Invoice Domain | The Invoice aggregate, lifecycle/state machine, finalization, immutability, and credit notes (avoir). |
| 4. Quote Domain | The Quote aggregate, its lifecycle, conversion to invoice. |
| 5. Numbering | Gap-free, sequential per-issuer document numbering. |
| 6. Quote Signature | E-signature capture for quotes. |
| 7. Reminders | Manual and automated reminder cadences. |
| 8. Payment Link and Collection | Payment links on Green-Got rails; collection. |
| 9. Transaction Matching | AR ↔ bank-transaction matching; full-amount settlement (TransactionMatched → Paid) and the VAT-collection ledger (PaymentCollected) that sources payment data (Enriched212Men for invoiced, Flux10 for non-invoiced). |
| 10. Tax and VAT | VAT category codes, rates, multi-currency, debits vs encaissement. |
6. Related Documents
- PA 2. Platform Architecture — the 5-corner model, Flux 1 / Flux 10, routing ownership.
- PA 5. Lifecycle Statuses — AFNOR XP Z12-012 statuses and the 3-layer mapping that
invoicingprojects. - PA 10. Integration Contracts — the authoritative crate-boundary and event contract.
- PA 4. Formats and Invoice Data — the canonical EN 16931 structured-invoice field list.
- bills/1. Bills Overview — the AP counterpart domain.
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:
- HT (hors taxes): net amount, excluding VAT. The taxable base.
- TTC (toutes taxes comprises): gross amount, VAT included. The amount payable.
- TVA (Taxe sur la Valeur Ajoutée): French VAT.
- VAT rate: the percentage applied to a taxable base. France’s standard and reduced rates are in §2.
- VAT category code: the EN 16931 UNCL5305 code qualifying why a line is taxed at its rate (standard, exempt, reverse charge, etc.). Defined canonically in PA 4. Formats and Invoice Data §5. Not redefined here — summarized in §3.
- VAT breakdown: the per-category, per-rate grouping of taxable base and VAT amount (EN 16931 BG-23). Mandatory on the invoice — see PA 4 §4.
- VAT on debits (TVA sur les débits): VAT chargeable when the invoice is issued. Mandatory for supplies of goods (livraisons de biens), and electable by a service provider who opts for it.
- VAT on collection / encaissement (TVA sur les encaissements): VAT chargeable when payment is collected. The default for supplies of services (prestations de services). It is not available for goods — a goods line is always on debits and cannot elect VAT-on-collection. The election runs one way only: a service provider may opt for débits; goods cannot opt for encaissement.
- Reverse charge (autoliquidation): the buyer, not the seller, accounts for the VAT. Category AE.
- Integer cents (i64): all monetary amounts are stored as integer cents, never floating point — consistent with the rest of the platform.
2. French VAT Rates
| Rate | Name | Typical application |
|---|---|---|
| 20 % | Taux normal (standard) | Default rate for most goods and services. |
| 10 % | Taux intermédiaire | Restaurants, transport, certain renovation works, etc. |
| 5.5 % | Taux réduit | Food staples, books, certain energy, etc. |
| 2.1 % | Taux particulier / super-réduit | Press, reimbursable medicines, certain specific cases. |
| 0 % | Zero / exempt / out-of-scope context | Carried 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:
- a default VAT rate — applied to every line of every invoice unless that line overrides it;
- a VAT-exemption flag — e.g. franchise en base de TVA for sole traders / small businesses, which makes the org’s invoices carry no VAT by default (category E, “TVA non applicable, art. 293 B du CGI”).
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:
| Code | Meaning | Line rate |
|---|---|---|
| S | Standard rate (incl. reduced positive rates) | Positive (20 / 10 / 5.5 / 2.1) |
| Z | Zero rated | 0 |
| E | Exempt (with statutory reference) | 0 |
| AE | VAT reverse charge (autoliquidation) | 0 |
| K | Intra-community supply of goods | 0 |
| G | Free export, tax not charged | 0 |
| O | Services outside the scope of VAT | 0 |
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.
| Case | Category | Who accounts for VAT | Mandatory mention | E-invoicing / e-reporting |
|---|---|---|---|---|
| Reverse charge (autoliquidation) | AE | The buyer | “Autoliquidation” mention; the buyer’s VAT number | Domestic B2B reverse charge stays on Flux 1; the seller reports no payment data (PA 7 §5). |
| Intra-EU supply of goods | K | The acquirer in the destination Member State | Exemption reference (e.g. CGI art. 262 ter I); both VAT numbers | Cross-border → out of e-invoicing scope, in e-reporting scope (Flux 10 transaction data). |
| Export (outside the EU) | G | Not charged (export) | Export exemption reference | Cross-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 channel — FrenchCompany, 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).
| Regime | VAT chargeable when | Applies to | Payment-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ébits | No payment data — chargeable event is issuance, not collection. |
| VAT on collection (sur les encaissements) | Payment is collected | Service lines (default); never goods | Yes — 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:
goods_services_composition—goodswhen every line is goods,serviceswhen every line is services,mixedwhen lines differ (3. Invoice Domain §2.1).- Document VAT-chargeability statement — when all lines share one regime, the document states that single regime (and the option mention if débits was elected). When lines differ, the document carries both regimes, each scoped to its lines, and the per-line regime is authoritative.
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:
| Group | vat_rate | vat_chargeability | taxable_base | vat_amount | reportability_state |
|---|---|---|---|---|---|
| Goods | 20 % | débits | €1,000 | €200 | not_reportable |
| Services | 20 % | encaissement | €500 | €100 | reportable |
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 overlay — 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). 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
- Per line — line net (HT) =
quantity × unit_price_net, in integer cents. Each line carries its VAT category code and rate (§3). - 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. - 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):
| Line | Description | Qty | Unit price HT | Line net HT | Category | Rate |
|---|---|---|---|---|---|---|
| 1 | Consulting (service) | 10 | €100.00 | €1,000.00 | S | 20 % |
| 2 | Printed book | 4 | €25.00 | €100.00 | S | 5.5 % |
| 3 | Intra-EU goods supply | 1 | €500.00 | €500.00 | K | 0 % |
VAT breakdown (per category/rate group):
| Group | Taxable base HT | Rate | VAT (rounded per group) |
|---|---|---|---|
| S @ 20 % | €1,000.00 | 20 % | €200.00 |
| S @ 5.5 % | €100.00 | 5.5 % | €5.50 |
| K @ 0 % | €500.00 | 0 % | €0.00 (intra-EU; acquirer accounts) |
Document totals:
| Total | Amount |
|---|---|
| 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).
- The invoice
currencyis a first-class field (PA 4 §4) carried on the aggregate; it is retained so future per-invoice (never per-line) multi-currency is possible without a model change. - When VAT is reportable and the currency is not EUR, the payment-data overlay carries the currency — on the enriched 212/MEN for an invoiced Flux-1 op, on Flux 10 only for a non-invoiced op (D-CARRIER) (PA 7 §3.3, PA 7 §6).
- For a settling match, the matched transaction currency must equal the invoice currency (9. Transaction Matching §8).
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
- Invariant: Totals are derived, never stored redundantly. The taxable bases, per-group VAT, total net, total VAT, and total gross are computed from line items + the VAT breakdown; they are not authoritative independent fields that could drift.
- Invariant: The invoice carries a per-category VAT breakdown (BG-23): for every (category, rate) group, the taxable base and the VAT amount. Total VAT equals the sum of the per-group VAT amounts.
- Invariant: VAT is rounded once per breakdown group, in integer cents; the breakdown reconciles exactly to the totals.
- Invariant: Every taxed line carries a VAT category code; a
0rate is always qualified by its category (Z / E / AE / K / G / O), never bare. - Invariant: The VAT chargeability regime (debits vs encaissement, plus the option mention when elected) and the goods-vs-services qualification are stored at line level as structured data; the document-level chargeability statement and
goods_services_compositionare derived from the per-line values. The per-line regime governs whether the collection of those lines produces a payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops). - Invariant: A goods line is always on débits and can never elect VAT-on-collection; only a service line may opt for débits. A goods line placed on encaissement is rejected at finalize.
- Invariant: All monetary amounts are integer cents (i64); no floating-point money anywhere in the computation.
9. Related Documents
- 2. Dual Invoice Model — the classical + EN 16931 structured invoice the VAT fields belong to.
- 3. Invoice Domain — line items, totals, and currency the computation runs over.
- 9. Transaction Matching — collection under the encaissement regime that triggers the payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops).
- PA 4. Formats and Invoice Data — the canonical VAT category codes (UNCL5305), the mandatory VAT-breakdown and chargeability-option mentions (not redefined here).
- PA 7. E-Reporting — when collection produces a payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops), and cross-border transaction (Flux 10) reporting.
10. Sources
- French VAT rates (taux de TVA en France): https://entreprendre.service-public.gouv.fr/vosdroits/F23567
- EN 16931 VAT category codes (UNCL5305) and mandatory mentions — via PA 4. Formats and Invoice Data (FNFE-MPE / EN 16931).
- VAT on debits vs encaissement and payment-data e-reporting: https://www.cleartax.com/fr/en/e-reporting-france
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:
- Thin DTO: the coarse
business_apishape exposed today — theInvoice,Quote, andClientsummary types insrc/services/business_api/src/routes/invoicing/models.rs. Sufficient for list/table display, insufficient to carry the legal invoice model. - Draft payload: the request/response shape of an invoice while it is editable (
status = Draft). Every field is mutable; mandatory EN 16931 terms may still be absent. - Finalized payload: the response shape of an invoice after finalize — number assigned, structured model generated, fields locked. Immutable; carries the full legal model.
- Typed legal field: a first-class, validated field for a legal identifier or structured value (SIREN, SIRET, EU VAT, address, VAT category, BT-21 note) — not a bare
String. The DTO carries the type so the contract is self-describing and the SDK is type-safe. - Status projection: the coarse
business_apistatus (Draft | Pending | Settled | Canceled) the API exposes, plus the reflected AFNOR legal status decorated onto it. Never the persisted canonical domain status (3. Invoice Domain §3.1). - Generated SDK: the frontend client generated from the API’s OpenAPI/
utoipaschema. The migration target that replaces hand-written fixtures and front-end-defined types.
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.
| Aspect | Draft payload (status = Draft) | Finalized payload (post-finalize) |
|---|---|---|
| Mutability | Every field editable; the only state a document may be deleted from. | Immutable; number, fields, and structured model locked (3. Invoice Domain §4). |
number | Absent (assigned at finalize). | Present — gap-free (BT-1), assigned at finalize. |
| Mandatory EN 16931 terms | May be incomplete — the draft is a work in progress. | All present — finalize fails if any mandatory term cannot be populated. |
| Structured model | Not generated. | Generated and locked; the transmission source of truth. |
| Totals | Display totals, recomputable. | Recomputed authoritatively at finalize; authoritative over display. |
| Corrections | Edit 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.
| Field | Type / shape | Validation | EN 16931 / source |
|---|---|---|---|
| Buyer SIREN | 9-digit identifier | length + 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 SIRET | SIREN (9) + NIC (5) | NIC validity; establishment designation | BT-47 variant (specific establishment). |
| EU VAT number | country prefix + body | intra-community format per country | BT-48 (buyer), BT-31 (seller). |
| Address | { line1, line2?, zip_code, city, country (ISO 3166-1 a2), state? } | mandatory country; structured, not a free-text block | BG-8 (billing), BG-15 / BT-75…BT-80 (delivery). |
| Legal form | enum/string (SAS, SARL, EI, …) | from a known set where applicable | seller/buyer legal block. |
| Currency | ISO 4217 | one per document; EUR-only for now | BT-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 finalize | Why |
|---|---|
number (BT-1) | Gap-free sequence; a renumbered/deleted entry breaks the audit trail (5. Numbering). |
| Line items, totals, tax breakdown | Frozen into the structured model; recomputed authoritatively at finalize. |
| Buyer identity snapshot, addresses, payment means | The 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.
- AR commercial status (presentation projection):
Draft | Pending | Settled | Canceled. This is the lossy, user-facing projection of the rich canonical internal status (3. Invoice Domain §3.1). It cannot distinguish platform rejection from buyer refusal, nor delivery from acceptance. - Reflected AFNOR legal status: the AFNOR XP Z12-012 code (200–213) the invoice projects from
plateforme_agreeeviaTransmissionStatusChanged, surfaced as a separate read-only field alongside the coarse status so the UI can show the precise legal stage when needed (3. Invoice Domain §6, PA 5. Lifecycle Statuses).
business_api status | Reached when (canonical internal) | Reflected AFNOR legal status |
|---|---|---|
| Draft | Draft | (none — pre-legal) |
| Pending | Issued → PaymentTransmitted (all transmitted/in-flight states) | the precise code 200–211, projected |
| Settled | Settled | Encaissée (212) (reflected) |
| Canceled | Refused / Rejected / Cancelled | Refusé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:
- Lift the schema. Define the full draft/finalized payloads, typed legal fields, line items, and BT-21 notes as
utoipa::ToSchemaDTOs inbusiness_api, replacing the front-end’s hand-written invoice types as the source of truth. - Generate the SDK. Produce the typed client from the OpenAPI document; the front-end imports generated types instead of locally-defined ones. The thin
Invoicesummary remains the list type; the new rich payloads back the single-invoice authoring/detail views. - Swap fixtures for the domain. Replace
fake_data.rsresponses with the persistedinvoicingdomain behind the same generated contract, route by route, so each endpoint flips from fixture to real without a schema change. - 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
- Invariant: The thin summary DTO is a list projection only; a single invoice is read/authored through the richer draft/finalized payload. The summary is derived from the full model, never authoritative.
- Invariant: Legal identifiers (SIREN, SIRET, EU VAT, country) are typed, validated fields, never interchangeable opaque strings; server-side validation is authoritative.
- Invariant: Per-line tax (
vat_category,rate,goods_or_service,vat_chargeability) is carried per line; the document-level chargeability and goods/services composition are read-only derived fields. - Invariant: Structured legal notes (BT-21) are typed
{ subject_code, text }fields with a UNCL4451 code; free-text/branding notes carry no subject code and are never a substitute for them. - Invariant:
number, line items, totals, tax breakdown, the buyer/address snapshot, payment means, and structured notes are immutable after finalize; the only mutation path is an avoir. Only aDraftmay be deleted. - Invariant: The API status (
Draft | Pending | Settled | Canceled) is a presentation projection, and the reflected AFNOR legal status is a projection from the PA; neither is client-writable, and the canonical AFNOR-aligned domain status is persisted in full. - Invariant: Finalize is a dedicated, strictly-validating action that assigns the gap-free number and locks the model — never a client-supplied
statustransition.
10. Related Documents
- 2. Dual Invoice Model — the classical vs EN 16931 structured model, the fields the thin summary omits, and the BT-21 structured legal notes.
- 3. Invoice Domain — the Invoice aggregate, finalization/immutability, the canonical state machine, the
business_apiprojection, and the avoir model. - 5. Numbering — gap-free numbering assigned at finalize (the locked
number). - 10. Tax and VAT — per-line VAT category, rates, debits vs encaissement, currency.
- PA 4. Formats and Invoice Data — the canonical EN 16931 mandatory mentions and code lists the typed fields populate.
- PA 5. Lifecycle Statuses — the AFNOR legal status set and the 3-layer mapping behind the reflected status.
- 9. Transaction Matching — the collection events behind the server-derived
Settledstatus and payment-data reporting.
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:
- Classical internal model: the human-facing invoice representation — the client, line items, notes, branding, and display totals an operator authors and edits. Optimised for UI and editing, not for transmission.
- EN 16931 structured model: the machine-readable representation defined canonically in PA 4. Formats and Invoice Data. The transmission source of truth: B2Brouter generates Factur-X / UBL / CII from it.
- Derivation: the one-way generation of the structured model from the classical model, performed at finalize time.
- Round-trip: reconciling structured data that arrives back (e.g. a buyer-side correction, or display of a transmitted invoice) against the classical model.
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:
- the classical internal model is what the UI binds to: it is editable while the invoice is a draft, it carries branding and display niceties, and it is the artefact a human reasons about;
- the EN 16931 structured model is generated from the classical model at finalize time and is the contract handed to
plateforme_agreeefor transmission.
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.
| Field | Type | Notes |
|---|---|---|
party | ClientType | One 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, phone | optional | Primary contact channels. |
billing_address | Address | EN 16931 BG-8: line1, line2?, zip_code, city, country (ISO 3166-1 a2), state?. |
delivery_address | optional Address | Default distinct delivery / lieu de livraison. An invoice may override it with its own delivery_address (see 3. Invoice Domain §2.1). |
buyer_reference | optional | The buyer’s PO / reference. |
document_language | ISO 639-1 | Default language for rendered documents. |
document_currency | ISO 4217 | Default currency. |
default_payment_terms_days | optional | Default settlement window. |
additional_contacts_count, contacts | Extra Contact records. | |
outstanding_amount, collected_amount | cents | Denormalised 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 group | Content | Purpose |
|---|---|---|
| Header | number (assigned at finalize), client reference, issue date, due date, document language/currency | The invoice identity and settlement terms. |
| Line items | per line: description, quantity, unit price (net), VAT rate, VAT category, line net | What is being billed. The editable list. |
| Notes | free-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. |
| Branding | logo, colours, template choice | The rendered look of the document (PDF face). |
| Display totals | total net, total VAT (per rate), total gross | Computed for display; recomputed authoritatively into the structured model at finalize. |
| Status | the internal lifecycle status + projected legal status | See 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:
- Buyer SIREN / SIRET — the Annuaire routing key, carried on the
Client(siren,nic) when the buyer is aFrenchCompany. It is mandatory only for domestic FR B2B (Flux 1): an invoice whose buyer is a French company but lacks a resolvable SIREN is not a valid domestic B2B e-invoice (PA 4 §4 invariant). It is not required for a B2C buyer (Individual) or aForeignCompany— those channels are e-reported over Flux 10 and carry no buyer SIREN. Party identity is conditional by channel —FrenchCompany,ForeignCompany,Individual, orPublicEntity— classified deterministically before finalization by the algorithm in §3.5.PublicEntity(B2G) and Monaco are out of MVP scope (§3.5). See also 10. Tax and VAT §4. - Distinct delivery address — mandatory when it differs from billing. Carried on the
Client(delivery_address) as the default; an invoice may carry its owndelivery_addressthat overrides theClientdefault for that document. - VAT category code (UNCL5305) — per line and per breakdown (S/Z/E/AE/K/G/O). See PA 4 §5 and 10. Tax and VAT.
- Operation category — the nature of the operation for VAT (standard supply, reverse charge, intra-EU, export, exempt, out of scope), expressed via the VAT category code, consistent with the goods-vs-services composition.
- Goods-vs-services composition — whether the operation covers livraisons de biens, prestations de services, or a mix.
- VAT-chargeability option (debits vs encaissement) — a document-level statement, including the explicit “option pour le paiement de la taxe d’après les débits” when elected. Governs whether collection triggers Flux 10 payment-data e-reporting (PA 10 §8).
- Structured payment means — IBAN / transfer / payment-means code (EN 16931 BG-16), not a free-text “pay to” line. Metadata only — the PA settles nothing.
- Document type (UNCL1001) — 380 for an invoice, 381 for an avoir, etc. See PA 4 §6.
- Structured legal notes — the mandatory B2B legal mentions (recovery fee, late-payment penalties, escompte/discount terms) carried as structured note fields, not free text. See §3.4.
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 code | Typical content | Default source |
|---|---|---|---|
| Recovery fee — indemnité forfaitaire de recouvrement | PMT | “Indemnité forfaitaire pour frais de recouvrement : 40 €” | Organisation default (fixed legal 40 €). |
| Late-payment penalties — pénalités de retard | PMD | Penalty rate / terms applicable on late payment | Organisation default (configurable rate/terms). |
| Discount / escompte terms | AAB | “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):
party == PublicEntity→ B2G → out of MVP (finalize rejects/parks).party == FrenchCompanyandbilling_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).party == FrenchCompanyandbilling_address.country == "MC"(Monaco) → treated as domestic in principle, but out of MVP (finalize rejects/parks).party == ForeignCompany(any non-FRbilling_address.country) → cross-border B2B → Flux 10 (e-reporting; no Annuaire routing, no buyer SIREN).party == Individual→ B2C → 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 term | Mandatory? | Note |
|---|---|---|---|
Seller org siren (from organisation/enrollment) | BT-30 (seller legal reg. id, scheme 0002) | Mandatory | The customer’s own SIREN; resolved from the organisation, not authored per invoice. |
| Seller name / address / VAT number (organisation) | BG-4, BT-31 | Mandatory | From the organisation profile. |
Client.party.siren | BT-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 variant | Conditional | When the buyer designates a specific establishment. |
Client name / billing_address / vat_number | BG-7, BT-48 | Mandatory | Buyer 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 flag | line/document qualification | Mandatory (must add) | Goods-vs-services composition. |
| Document VAT-chargeability statement | document-level statement | Mandatory when applicable (must add) | Debits vs encaissement; option mention. |
| Per-line / breakdown VAT category | BT-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-1 | Mandatory | Gap-free; see 5. Numbering. |
issued_at | BT-2 | Mandatory | Issue date. |
due_at / payment terms | BT-20 / BT-9 | Mandatory | Settlement terms + due date. |
| Structured payment means (IBAN, means code) | BG-16 (BT-81, BT-84…) | Mandatory (must add) | Metadata only; PA settles nothing. |
| Line: description | BT-153 | Mandatory | |
| Line: quantity | BT-129 | Mandatory | |
| Line: unit price (net) | BT-146 | Mandatory | |
| Line: line net amount | BT-131 | Mandatory | qty × unit price. |
| Line: VAT category + rate | BT-151 / BT-152 | Mandatory | |
| Display total net | BT-109 | Mandatory | Recomputed authoritatively at finalize. |
| VAT breakdown per rate | BG-23 (BT-116…BT-119) | Mandatory | Taxable base + VAT per category/rate. |
| Display total VAT | BT-110 | Mandatory | |
| Display total gross | BT-112 | Mandatory | Amount payable. |
currency | BT-5 | Mandatory | ISO 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
- Display of a transmitted invoice. After finalize, the UI renders from the classical model; the structured model and the projected legal status (from
TransmissionStatusChanged) decorate it. The classical model is never edited post-finalize — corrections issue an avoir (see 3. Invoice Domain). - No reverse edit. There is no path that edits the structured model and writes back to the classical model. Derivation is one-way (classical → structured). The structured model is regenerated only by issuing a new document (a corrected invoice draft or an avoir), never by mutating a finalized one.
- Consistency with the PDF face. For Factur-X, the human-readable PDF (rendered from the classical model) and the embedded XML (the structured model) must be consistent; the structured XML is the authoritative data, the PDF is its face (PA 4 §3.1). Because both derive from the same finalized classical model, they cannot diverge.
- Inbound structured data is NOT a round-trip into invoicing. Structured supplier-invoice data that arrives via
InboundInvoiceReceivedis AP and lands inbills; it never updates aninvoicingclassical model. See PA 10 §4.
8. Related Documents
- PA 4. Formats and Invoice Data — the canonical EN 16931 structured-invoice field list (not redefined here).
- 3. Invoice Domain — the Invoice aggregate, finalization, immutability, and avoir.
- 5. Numbering — gap-free numbering assigned at finalize.
- 10. Tax and VAT — VAT category codes, rates, currency (EUR-only for now), debits vs encaissement.
- PA 10. Integration Contracts §5 — the structured-invoice handoff from
invoicingtoplateforme_agreee.
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:
- Invoice (aggregate): the AR commercial-invoice aggregate (“inv_…”), document type 380. Carries the classical model and, after finalize, the immutable EN 16931 structured model.
- Finalization: the transition that assigns a gap-free number, locks the editable fields, generates the structured model, and emits
InvoiceFinalized. The point at which the invoice becomes the immutable AR record. - Avoir (credit note): the corrective AR aggregate (“avo_…”), document type 381, that reverses or adjusts a finalized invoice. Has its own lifecycle and its own gap-free number.
- Canonical internal status: the rich domain status set persisted in
invoicing— a 1:1 image of the full AFNOR 14-status set plus the pre-legal states (Draft,Issued,Submitted, …,Settled,Rejected,Cancelled). The source of truth for the AR document’s own state. - business_api projection: the coarse status the HTTP API exposes (
Draft | Pending | Settled | Canceled). A presentation projection of the canonical internal status, never the persisted domain state. - Legal status (reflected): the AFNOR XP Z12-012 legal status the invoice projects from
plateforme_agreeeviaTransmissionStatusChanged. Carried on the aggregate aslegal_status. Never owned byinvoicing.
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.
| Field | Type | Notes |
|---|---|---|
id | “inv_…” | Prefixed, time-sortable. |
number | string, optional until finalize | Gap-free document number (BT-1), assigned at finalize. See 5. Numbering. |
document_type | UNCL1001 = 380 | Commercial invoice. See PA 4 §6. |
client_id | “cli_…” | The buyer party. Carries buyer SIREN/SIRET (mandatory routing key) and the billing/delivery addresses. |
client_name | string | Denormalised display name. |
status | canonical internal status | See §3. |
legal_status | AFNOR XP Z12-012, nullable | Reflected AFNOR legal status, projected from plateforme_agreee via TransmissionStatusChanged. See §6. |
transmission_id | FK to PaTransmission, nullable | Back-reference to the plateforme_agreee transmission carrying the authoritative legal status. Owned by PA, referenced here. |
line_items[] | per line | Description (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_address | Address, nullable | Per-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 | mixed | Derived 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 | mixed | Derived 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_means | structured (IBAN, means code) | BG-16; metadata only. |
totals | total net (BT-109), VAT breakdown per rate (BG-23), total VAT (BT-110), total gross (BT-112) | Recomputed authoritatively at finalize. |
currency | ISO 4217 (BT-5) | One currency per invoice; EUR-only for now. Field retained for future per-invoice multi-currency. See 10 §7. |
notes, branding | classical-only | Not EN 16931 terms; PDF face only. |
issued_at, due_at, created_at | timestamps | Issue 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:
- AR-commercial status (
statuscolumn, Green-Got-owned):Draft | Issued | Sent | Viewed | Overdue | Paid | Void. This is the axis thesend_invoice, viewed-callback,mark_overdue,mark_paid, andvoid_invoicetransitions drive.Sent,Viewed, andOverduehave no AFNOR equivalent (AFNOR has no “viewed” or “overdue” code);Paidis set by a full-amount match;Voidis a commercial void, distinct from AFNORCancelled/Refused. Timestamp columnssent_at/viewed_at/paid_atrecord these transitions. - AFNOR legal status (
legal_statuscolumn, reflected, never authored by invoicing): the AFNOR XP Z12-012 set (200–213) plus the rareCancelled, projected fromplateforme_agreeeviaTransmissionStatusChanged.Settled(Encaissée 212) andCancelled(Annulée) live strictly on this legal axis. The legalSettledis the legal-axis mirror of the commercialPaid: Green-Got’s collection drives both —mark_paidsets the commercialPaid, 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:
Draftis the only editable state and the only state a document may be deleted from. Everything pastIssuedreflects a finalized, immutable invoice.- The status set mirrors PA/5 §9 (the OUT rows of PA 5 §10) exactly; this document does not re-derive it. The invoice only projects the AFNOR legal status it is told about via
TransmissionStatusChanged(see §6). - Transitions from
Submittedonward are driven by the reflected legal status (TransmissionStatusChanged), exceptSettled, which is driven by Green-Got’s own collection data (B2Brouter settles nothing — see 9. Transaction Matching). Rejected(platform, Rejetée 213) andRefused(buyer, Refusée 210) are different rejections at different layers and must never be conflated — see PA 5 §4.- The recommended intermediate statuses (
Transmitted,Available,TakenInCharge,PartiallyApproved,Suspended,Completed,PaymentTransmitted) arrive as CDAR codes on the transmission and are projected verbatim; B2Brouter’s narrower native emission is the PA adapter’s concern, not the invoicing domain’s (PA 5 §10 design rule). Cancelledis exceptional. The compliant way to reverse a finalized invoice is an avoir (§5), not cancellation.
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 projection | Reflected AFNOR legal status (code) |
|---|---|---|
Draft | Draft | (none — pre-legal) |
Issued | Pending | (none — created in PA, pre-send) |
Submitted | Pending | Déposée (200) |
Transmitted | Pending | Émise par la plateforme (201) |
Delivered | Pending | Reçue par la plateforme (202) |
Available | Pending | Mise à disposition (203) |
TakenInCharge | Pending | Prise en charge (204) |
Accepted | Pending | Approuvée (205) |
PartiallyApproved | Pending | Approuvée partiellement (206) |
Disputed | Pending | En litige (207) |
Suspended | Pending | Suspendue (208) |
Completed | Pending | Complétée (209) |
PaymentTransmitted | Pending | Paiement transmis (211) |
Settled | Settled | Encaissée (212) |
Refused | Canceled | Refusée (210) |
Rejected | Canceled | Rejetée (213) |
Cancelled | Canceled | Annulé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:
- 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 == goodscarriesvat_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). - Assign the gap-free number (BT-1) from the issuer’s sequence. Numbering is sequential and gap-free per 5. Numbering.
- Generate the EN 16931 structured model from the classical model (2. Dual Invoice Model §6). This is the transmission source of truth.
- Lock the classical and structured models — the invoice becomes immutable.
- Emit
InvoiceFinalizedon the eventbus (invoice_id,organisation_id, structured-invoice reference) soplateforme_agreeebegins 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
| Field | Type | Notes |
|---|---|---|
id | “avo_…” | Prefixed (avo_), time-sortable. |
number | string | Its own gap-free, year-prefixed number (e.g. AV-2026-0001), from a sequence distinct from invoices. See 5. Numbering. |
document_type | UNCL1001 = 381 | Avoir / 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 line | The amounts being credited (full reversal or partial adjustment), with their VAT category + rate so the credited VAT is explicit. |
totals | net / VAT breakdown / VAT / gross | Same EN 16931 totals structure; represents the credited (negative) amount, including the credited VAT per 10. Tax and VAT. |
settlement_mode | refund | credit_applied | How the credit is settled: refunded (transfer/cheque to the buyer) or applied as a credit against a future invoice. See §5.3. |
status | avoir internal status | See §5.2. |
legal_status | AFNOR, nullable | Reflected AFNOR legal status, projected from its own transmission via TransmissionStatusChanged. |
transmission_id | FK to PaTransmission, nullable | Its own outbound transmission. |
issued_at, created_at | timestamps |
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:
- Refund — the seller pays the credited gross back to the buyer (transfer/cheque). The original invoice may already be
Settled(Encaissée); the avoir reverses value already collected. - Credit applied — the credit is held against the buyer and imputed on a future invoice, reducing its payable amount rather than moving money now.
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.
| Field | Type | Notes |
|---|---|---|
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_amount | integer cents (i64) | The TTC amount of avoir credit imputed onto this invoice (with its VAT split, mirroring the avoir’s breakdown). |
applied_at | timestamp | When 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:
- Refund — the credited VAT is recovered by imputation or restitution on the seller’s next VAT return (cadre 3B régularisations / déduction), conditional on the avoir referencing the original invoice and stating the HT discount + corresponding VAT (CGI art. 271/272; the avoir must justify the correction to the administration).
- Credit applied — the credited VAT is netted at the moment of imputation onto the future invoice: the future invoice’s collected VAT base is reduced by the imputed VAT when the
CreditAllocationis recorded, so the regularisation rides on the future invoice’s own encaissement reporting rather than a standalone refund-return adjustment.
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
- Entity —
Avoiraggregate (DECISION D8), id prefixavo_, fields per §5.1. The entity name isAvoireverywhere ininvoicing— there is noCreditNotetype; “credit note” is only the English gloss. It mirrors theInvoiceaggregate (classical model + derived EN 16931 structured model) withdocument_type = 381,corrects_invoice_id, andsettlement_mode. Where theplateforme_agreeemodel refers to a type-381 document, it is the same document thisAvoirproduces (transmitted over Flux 1 like any invoice); the PA side carries no separate credit-note entity. The original invoice is immutable and is never touched. - Sequence — a dedicated gap-free, year-prefixed
AV-YYYY-NNNNsequence, assigned at finalize, independent of the 380 and quote sequences (see 5. Numbering). - Lifecycle — author as
Draft→ finalize (assign 381 number, derive + lock structured model) → transmit viaplateforme_agreeeover Flux 1 → project AFNORlegal_statusfromTransmissionStatusChanged→ reach its own Encaissée-equivalent terminal state on refund-reconciliation or credit-consumption (§5.2). - Link to original —
corrects_invoice_idreferences the finalized invoice; the (380 + 381) pair is the audit-faithful correction record. An avoir may reference only a finalized invoice.
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
- 2. Dual Invoice Model — classical model, EN 16931 structured model, derivation at finalize.
- 5. Numbering — gap-free, sequential numbering for invoices (380) and avoirs (381).
- 9. Transaction Matching — AR ↔ bank-transaction matching; the Encaissée / Settled trigger.
- PA 4. Formats and Invoice Data — mandatory mentions; document type codes (380/381).
- PA 5. Lifecycle Statuses — AFNOR statuses, the 3-layer mapping, and the canonical internal status set.
- PA 8. Archiving and Audit — retention, the piste d’audit fiable, and where the transmitted issued-invoice copy is held.
- PA 10. Integration Contracts — the outbound contract,
InvoiceFinalized/TransmissionStatusChanged, and the AR/AP boundary.
8. Sources
- Facture d’avoir — mandatory mentions, reference to the original invoice, HT + corresponding VAT, “net de taxe” case: BOFiP TVA — mentions spécifiques: https://bofip.impots.gouv.fr/bofip/1531-PGP.html/identifiant=BOI-TVA-DECLA-30-20-20-30-20190925
- Avoir — VAT treatment on the declaration (imputation vs restitution, cadre 3B régularisations): impots.gouv.fr — “Comment traiter une facture d’avoir sur ma déclaration de TVA ?”: https://www.impots.gouv.fr/professionnel/questions/comment-traiter-une-facture-davoir-sur-ma-declaration-de-tva
- Avoir — gap-free sequential numbering (dedicated vs interleaved sequence) and settlement modalities (refund / credit voucher / reduction on next invoice): https://www.legalplace.fr/guides/facture-avoir/
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
- Quote (devis): a commercial offer Green-Got issues on behalf of an organisation to one of its clients, stating proposed line items and a price, valid until an expiry date. A quote is not a fiscal invoice and carries no e-invoicing obligation.
- Devis: French for quote. Used interchangeably with “quote” in this domain.
- Organisation: the Green-Got business customer that issues the quote. Owns
OrganisationId(defined in theorganisationcrate). - Client: the organisation’s customer — the recipient of the quote. Modelled by the
Clientaggregate (cli_id); see the invoicing data model. - Line item: a single priced line of the quote (description, quantity, unit price, VAT rate). Shared shape with invoice line items.
- Validity period: the window during which the quote can be accepted; expressed as a validity duration and a resolved
expires_atdate. - E-signature: electronic signature of the quote by the client. Performed by the external Green-Got service
ggbs(see 6. Quote Signature); this domain consumes the outcome only. - SES (Simple Electronic Signature): the lowest eIDAS signature level used for commercial quotes;
ggbsoffers levels from SES up to a HIGH level using Green-Got face-recognition. Defined in 6. Quote Signature. - Conversion: turning a quote (signed or not) into a new Draft invoice by copying its line items and client.
- Internal model: Green-Got’s own quote representation. A quote is not an EN 16931 structured document and has no Factur-X / UBL / CII obligation, unlike an issued invoice.
- Source of truth: for a given fact, the single layer that authoritatively defines it.
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:
- The quote header (number, client, currency, validity, status, timestamps).
- Its line items.
- A reference to its in-flight or completed signature (
signature_id). - A reference to the invoice it was converted into (
converted_invoice_id), if any.
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
| Field | Type | Notes |
|---|---|---|
id | QuoteId (quo_…) | Primary key; time_sortable_id. |
organisation_id | OrganisationId | Owning organisation. |
client_id | ClientId (cli_…) | Recipient client. |
number | Option<QuoteNumber> | e.g. DEV-2026-0012. None while Draft; assigned at send. See §4 and 5. Numbering. |
status | QuoteStatus | Canonical domain status; see §3. |
currency | ISO 4217 | Document currency. |
issue_date | Option<Date> | Set when sent. |
validity_days | i16 | Validity duration; default 30 days. |
expires_at | Option<Date> | Resolved expiry = issue_date + validity_days. |
line_items | Vec<LineItem> | Description, quantity, unit price (cents, i64), VAT rate. |
total_amount | cents (i64) | Computed gross (TTC) from line items; denormalised for listing. |
note | Option<String> | Free-text note shown on the quote. |
signature_id | Option<SignatureId> | Reference to the in-flight / completed signature record (see 6. Quote Signature). |
converted_invoice_id | Option<InvoiceId> | Set on conversion; prevents double-conversion. |
pdf_file_id | Option<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_name | String | Denormalised client name for listings. |
created_at | timestamptz | |
updated_at | timestamptz | |
sent_at | Option<timestamptz> | First send. |
viewed_at | Option<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_at | Option<timestamptz> | Set from the signature outcome. |
Validation rules:
- A quote references exactly one
client_idand at least one line item before it may leaveDraft. - Money is stored as integer cents (
i64); totals are computed from line items, never authored independently. currencyis ISO 4217;validity_daysis a positive integer.
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 status | Canonical domain status(es) it projects |
|---|---|
| Draft | Draft |
| Pending | Sent, Viewed |
| Signed | Signed |
| Cancelled | Declined, 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:
- Draft is the only state with no number and the only state from which a quote may be deleted (a Draft has no fiscal trace). Every other terminal exit is via archival to
Cancelled, not deletion. - Send is the transition that assigns the number, sets
issue_dateandexpires_at, renders the PDF, and sends the quote for signature. - Viewed is informational (it records that the client opened the quote / signing link); it does not change what the organisation may do.
- Signed, Declined, and Expired are produced by the signature outcome / validity expiry, never authored directly by a route handler.
- Conversion is allowed from any live sent quote, signed or not: a quote may have been accepted outside our signature flow, so conversion to an invoice is an explicit organisation action available from
Sent,Viewed, orSigned— but never fromExpired(see the next bullet). See §5. - Conversion is an action, not a lifecycle transition. Converting sets
converted_invoice_idand does not change the quote’s status; the quote keeps the status it was in (Signed,Viewed,Sent). This resolves the apparent contradiction that aSignedquote can later beCancelled:Signedis display-terminal (no further signature-outcome transition) but not absorbing — it retains a single exit, archival toCancelled. OnlyCancelled(and the unnumberedDraft → delete) is an absorbing terminal. - An
Expiredquote is non-signable AND non-convertible — the only forward action is re-issue (AR-3). Once a quote passes itsexpires_atit can no longer be signed or converted; the offer has lapsed. The organisation re-issues a new quote based on the expired one (a fresh quote with its own number and a fresh validity window, sent for signature afresh). There is no direct conversion / late acceptance of an expired quote. See §5.1. - A Declined quote may be re-sent (a fresh signature request; the previous signature record is archived). An Expired quote is re-issued as a new quote (Expired → Sent), which likewise opens a fresh validity window and new signature request.
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 Signed — not 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:
- Creates a new
Invoicein status Draft (no invoice number yet — invoice numbers are assigned only at finalize; see 3. Invoice Domain and 5. Numbering). - Copies the quote’s
client_id,currency, and line items into the new invoice. - Records the linkage on both sides: the invoice references the originating
quote_id; the quote recordsconverted_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:
- On send for signature (from the quote UI): render the quote PDF, create a signature request against it via
ggbs, and persist the resultingsignature_idon the quote.ggbsemails the client a link that opens a tab to consult and sign the quote. - On outcome: receive a signed / declined / expired outcome (plus an audit-trail reference) and drive the corresponding quote transition (
Signed/Declined/Expired). - Persist, against the quote, what the contract returns: the signature reference, the audit-trail file reference, and
signed_at.
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
- 3. Invoice Domain — the invoice a signed quote converts into; where the regulated e-invoicing lifecycle begins.
- 5. Numbering — the legally-compliant, concurrency-safe document numbering scheme (quotes use the
DEVseries). - 6. Quote Signature — the e-signature flow, contract, and levels invoicing depends on (performed by the external
ggbsservice). - 7. Reminders — reminder cadence for unsigned quotes and unpaid invoices.
- ../../plateforme_agreee/docs/5_lifecycle_statuses.md — the regulated AFNOR / B2Brouter / internal status model that applies to the invoice after conversion, not the quote.
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:
- French invoice/devis numbering and mandatory mentions — see 5. Numbering §11 Sources and PA — 4. Formats and Invoice Data.
- eIDAS electronic-signature levels (SES sufficient for commercial quotes, Art. 25) — see 6. Quote Signature §Sources.
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
- Numbering series: an independent, ordered sequence of document numbers scoped to one organisation and one document type (and, when annual reset is enabled, one year). Each series produces a contiguous run of numbers.
- Document type: the kind of document a series numbers — issued invoice, quote, or credit note (avoir). Each has its own series.
- Avoir (credit note): a corrective document (UNCL1001 document type code 381) that reverses or adjusts a finalized invoice. Credit notes are numbered from their own gap-free, year-prefixed series (e.g.
AV-2026-0001), separate from invoices and quotes. The avoir entity / model (fields,corrects_invoice_idlinkage, lifecycle,avo_id prefix) lives in 3. Invoice Domain; only its numbering is specified here. - Sequence number: the monotonically increasing integer within a series.
- Number: the rendered identifier shown on the document, e.g.
FAC-2026-0001, composed of{prefix}-{year}-{sequence}. - Finalization: the act of turning a Draft invoice into a legally issued invoice — the point at which a number is assigned. For quotes the equivalent point is send. See 3. Invoice Domain and 4. Quote Domain.
- Gap-free (sequential): the legal property that a series contains no holes — every integer between the first and the last issued number exists, with none skipped or reused.
- Legal entity: the organisation (
OrganisationId) on whose behalf the document is issued; the unit to which the gap-free requirement legally attaches. - Source of truth: for a given fact, the single layer that authoritatively defines it.
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:
- A finalized invoice’s number is permanent. It cannot be changed, and the slot it occupies cannot be reassigned.
- A finalized invoice cannot be deleted to “free up” its number — doing so would create a gap. Corrections are made with a credit note (avoir), never by deletion. See §7.
- The allocator must be chronological: number n+1 must be issued after number n in time, within a series.
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}:
| Component | Description | Example |
|---|---|---|
| prefix | A short, document-type-specific, per-organisation-configurable string. | FAC, DEV, AV |
| year | The four-digit year of issuance. | 2026 |
| sequence | Zero-padded sequence number within the series, monotonically increasing. | 0001, 0012 |
| Document type | Default prefix | Example number | Series |
|---|---|---|---|
| Issued invoice | FAC | FAC-2026-0001 | Invoice series |
| Quote (devis) | DEV | DEV-2026-0012 | Quote series |
| Credit note (avoir, type 381) | AV | AV-2026-0003 | Distinct 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):
- Allocation is serialised per series — for example via a Postgres transaction-scoped advisory lock keyed on
(organisation_id, document_type, year?), or an equivalent atomic upsert on a counter row keyed the same way. Either mechanism guarantees one winner at a time per series. - The counter is persisted, so a process restart never reuses a number.
- The allocation runs inside the same database transaction as the finalization that consumes it (see §6), so a number is committed only if the document it numbers is committed.
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:
- Invoice: the number is assigned at finalize, inside the same transaction that sets the invoice to its first issued status and writes
number. Before finalize, an invoice isDraftwithnumber = None. See 3. Invoice Domain. - Quote: the number is assigned at send, inside the same transaction that records the send. Before send, a quote is
Draftwithnumber = None. See 4. Quote Domain. - Credit note: the number is assigned from the credit-note series at the point the credit note is issued against its target invoice.
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-*):
| Rule | Constraint | Applies to |
|---|---|---|
| BR-FR-01 | The invoice identifier must be at most 35 characters. | BT-1 (and BT-25) |
| BR-FR-02 | Alphanumeric 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-02 | The 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:
- exceeds 35 characters at any reachable sequence (BR-FR-01),
- contains characters outside the BR-FR-02 set (e.g. a prefix with spaces or accented characters), or
- could collide on the
(number, year, SIREN)uniqueness key — e.g. two series for the same organisation sharing a prefix and year and sequence space, or a mandate setup whose numbers lack a mandataire-specific root.
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:
| Identifier | What it is | When it exists | Mutable? | 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.
| Identifier | Minted by | Stored by Green-Got | Sent 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 id | B2Brouter | Yes — 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
- 3. Invoice Domain — invoice finalization (where invoice numbers are assigned) and the credit-note (avoir) entity/model (the avoir’s numbering is owned here, in §7).
- 4. Quote Domain — quote send, the transition where quote numbers are assigned, and the
DEVseries.
11. Sources
- Mandatory invoice fields & numbering (gap-free, chronological, per-series) — French e-invoicing reform mandatory mentions, including document type codes UNCL1001 380 (commercial invoice) / 381 (credit note / avoir): service-public — facturation électronique (A15683) ; facturwise — mandatory invoice fields France.
- Invoice-identifier rules (BR-FR-01 35-character limit, BR-FR-02 allowed characters, BR-FR-CO-02 uniqueness triple) — AFNOR XP Z12-012, Spécifications externes B2B (business rules annex): impots.gouv.fr — norme AFNOR factures (XP Z12-012). Uniqueness numbering also per BOFIP BOI-TVA-DECLA-30-20-20-10.
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
ggbs: the external Green-Got service that performs the e-signature — collecting the signer’s signature on the quote PDF, producing the signed document and an audit trail. The same Green-Got business unit already provides VOP (Verification of Payee) and FNCRF services; it also offers a HIGH signature level backed by Green-Got’s face-recognition software. Lives outside the invoicing crate; invoicing depends on its interface only.- Signature flow: the end-to-end path — from the quote UI the customer sends the quote for signature → the client receives an email with a link → the link opens a tab to consult and sign the quote.
- Signature request: the unit of work invoicing submits to
ggbs: a quote PDF plus the signer and metadata, to be signed. - Signature outcome: the result
ggbsreturns asynchronously — signed, declined, or expired — plus references. - Audit trail: the legally significant evidence record
ggbsproduces for a completed signature (signer identity, timestamps, IP, consent, document hash). It is PII-bearing (and, at the HIGH level, backed by face-recognition evidence), so it is retained under the four-basis retention model — statutory window, then product archive promise, with permanent retention limited to non-PII integrity evidence (the document content hash) — not “forever” as PII (see §6 and PA — 13. Privacy §2, §4). - SES (Simple Electronic Signature): the lowest eIDAS signature level — electronic data attached to / logically associated with other data, used by the signer to sign. Sufficient for commercial quotes under eIDAS Art. 25.
- HIGH level (face recognition): the highest assurance level
ggbsoffers for a quote signature, binding the signer’s identity via Green-Got’s face-recognition software. Used when an organisation requires stronger identity assurance than SES. - eIDAS: EU Regulation 910/2014 on electronic identification and trust services. Art. 25 establishes the legal effect of electronic signatures.
- Signature reference: the opaque identifier of the signature request/instance in
ggbs, persisted by invoicing to correlate callbacks. - Source of truth: for a given fact, the single layer that authoritatively defines it.
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:
- The signature flow (quote UI → send for signature → client email/link → consult & sign).
- The request/callback contract between invoicing and
ggbs. - The signature levels invoicing may request (SES up to the HIGH face-recognition level).
- What invoicing persists about a signature.
- How a signature outcome drives the 4. Quote Domain state machine.
Out of scope here (owned by ggbs):
- The
ggbsHTTP endpoints, request bodies, multi-step request creation, document upload, and signer/field placement. - The
ggbs-internal signing scheme and webhook event-name vocabulary (the wire details of howggbssigns and labels its callbacks). - The face-recognition implementation behind the HIGH level, environments, API keys, and rate limits.
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:
- From the quote UI, the customer sends the quote for signature (this is the
Senttransition in 4. Quote Domain §3: the number is assigned and the PDF rendered, then the signature request is submitted toggbs). ggbsemails the client a link.- The link opens a tab where the client can consult and sign the quote (at the requested level — see §3).
ggbsreturns 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:
- SES (Simple Electronic Signature) — the default level for commercial quotes. Under eIDAS Article 25, an electronic signature is not denied legal effect solely because it is electronic or not qualified; SES is admissible and sufficient for commercial quotes.
- HIGH (face recognition) — the high-assurance level, which binds the signer’s identity via Green-Got’s face-recognition software. Requested when an organisation needs stronger identity assurance than SES.
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:
| Field | Description |
|---|---|
quote_id | The quote being signed; used to correlate the eventual outcome. |
| document | The rendered quote PDF (or a reference to it in the file store). |
| signer | The client signer’s identity (name, email). |
| signature level | SES by default, or the HIGH face-recognition level (see §3). |
| expiry | The 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:
| Outcome | Carries | Quote transition |
|---|---|---|
| signed | signer reference, signed_at, audit-trail reference | Sent/Viewed → Signed |
| declined | signer reference, reason? | Sent/Viewed → Declined |
| expired | — | Sent/Viewed → Expired |
| viewed (informational) | — | Sent → Viewed (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):
- Constant-time signature verification. Verify the
ggbscallback signature over the raw request body (no re-serialization) using a constant-time compare; reject and alert on any failure. The signing secret comes from the secret store, never source/env-in-git. - Timestamp freshness (replay window). Reject callbacks whose timestamp is outside a bounded freshness window (past or future skew). Replay protection is mandatory, not optional.
- Processed-event store. Persist a stable per-delivery identity (event id, else signature, else a hash of
(timestamp, raw_body)); a delivery whose identity is already recorded is idempotently acked-and-dropped. This bounds replay even within the freshness window. - Raw-body hash only. Record a hash of the raw callback body for dedupe and tamper-evidence; the raw body itself is not durably persisted (short-TTL encrypted vault only if genuinely needed for investigation), per the field-level retention rule (PA — 13. Privacy §4.1).
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 field | Purpose |
|---|---|
signature reference (signature_id) | Correlates callbacks to the quote; opaque handle into ggbs. |
| signature level | The level requested / performed (SES or HIGH face-recognition). |
| audit-trail file reference | Reference to the stored audit-trail document (the evidentiary record). |
signed_at | Timestamp 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). |
| status | The 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:
- Statutory legal-obligation window — kept for the 6-yr VAT / 10-yr accounting period (the evidence-of-agreement minimum).
- 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.
- Permanent only for non-PII integrity evidence — only the document content hash (and other non-PII integrity evidence) is kept permanently outright.
- 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
- eIDAS — electronic signature legal effect (SES sufficiency) — Regulation (EU) No 910/2014, Article 25: eIDAS Art. 25 — EUR-Lex.
- 10-year accounting retention (minimum) — French e-invoicing reform archiving guidance: vatupdate — France e-invoicing AFNOR archiving (FD Z42-029) ; see also ../../plateforme_agreee/docs/8_archiving_and_audit.md.
9. Related Documents
- 4. Quote Domain — the quote aggregate whose state machine the signature outcome drives; the signature handoff at the domain level.
- 5. Numbering — quote numbering, assigned at send before the signature request.
- ../../plateforme_agreee/docs/8_archiving_and_audit.md — archiving and audit-trail retention obligations.
- The signature itself is performed by
ggbs— the external Green-Got service that also provides VOP and FNCRF — which owns everything flagged out of scope in §1 (its endpoints, callback verification, and the face-recognition implementation behind the HIGH level).
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:
- Reminder: a single outbound communication that asks the recipient to act on an issued invoice (pay) or a quote (sign before expiry). A reminder is not a new legal document; it carries no AFNOR lifecycle status and is never transmitted over Flux 1.
- Cadence: the ordered set of scheduled offsets at which automatic reminders fire for a given invoice or quote, expressed relative to an anchor date (the due date for invoices, the expiry date for quotes).
- Anchor date: the reference date a cadence offset is measured from. For an invoice it is
due_at; for a quote it isexpires_at. - Offset: a signed day count relative to the anchor.
D-3= three days before the anchor;D+7= seven days after. - Overdue: the state of an issued invoice whose
due_athas passed without settlement. Overdue is a derived condition, not a stored AFNOR status — see §7. - Org user: a user of the Green-Got customer organisation (the seller) that issued the invoice. Reminders notify org users in-app (push) so they can intervene.
- Client: the customer’s client (the buyer) who owes the invoice. Reminders are sent to the client by email.
- Skip condition: a state that suppresses a scheduled reminder (paid, void/cancelled, refused/disputed). See §6.
- Settlement: confirmation that the invoice is paid in full (internal status
Settled, AFNOR Encaissée), driven by 9. Transaction Matching. - Reminder run: one evaluation of the cadence for one invoice/quote at a scheduled offset, which either sends a reminder or skips it.
- Reminder mode: the per-document reminder behaviour the customer chooses before sending the quote or invoice — automatic (Green-Got’s default cadence), a custom cadence (the customer’s own offsets), or none (no automatic reminders; manual only). See §3.2.
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 case | Trigger | Recipient | Cadence-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 cadence | A 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.
- The action is gated by the same eligibility check as the automatic cadence (§6); the button is disabled (or the action rejected) when the invoice is in a skip state.
- A manual reminder is recorded as a reminder run with
source = Manualso that subsequent automatic runs and the audit trail account for it. - Sending a manual reminder does not reset or shift the automatic cadence; the cadence offsets remain anchored to
due_at/expires_at. A manual reminder is always available regardless of the document’s reminder mode — even when the customer chose none, the org user can still chase a client by hand.
3.2 Automatic cadence
Reminders are fully configurable per document. Before sending a quote or invoice, the customer chooses its reminder mode:
- Automatic — Green-Got applies its default cadence (§4).
- Custom cadence — the customer overrides the offsets (adds, removes, or moves them).
- None — no automatic reminders fire; the org user may still send manual reminders.
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).
- Each cadence offset that elapses produces at most one reminder run for that invoice.
- A run that lands on an invoice in a skip state is recorded as skipped and sends nothing.
- The scheduler is Temporal (already in the Green-Got stack — see §9): per-document Temporal timers fire each cadence offset, and a daily Temporal sweep over Postgres catches any due reminder a timer missed. It is independent of B2Brouter webhooks and of the Flux 1 lifecycle.
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.
| Step | Offset | Relative to | Tone / intent | Channels |
|---|---|---|---|---|
| 1 | D-3 | 3 days before due | Friendly upcoming-payment notice | Email to client + push to org user |
| 2 | D-day | On the due date | Due-today reminder | Email to client + push to org user |
| 3 | D+7 | 7 days overdue | First overdue reminder | Email to client + push to org user |
| 4 | D+14 | 14 days overdue | Second overdue reminder, firmer | Email to client + push to org user |
| 5 | D+30 | 30 days overdue | Final overdue reminder; flag for manual escalation | Email to client + push to org user |
Key characteristics:
- The cadence is bounded: after the last configured offset (default
D+30) no further automatic reminders are sent. Continued chasing past the final step is a manual action (§3.1) or an out-of-band collections decision. - Offsets are evaluated against the invoice’s own
due_at, so each invoice has its own timeline. - Any offset whose evaluation finds a skip condition is recorded as skipped, and the cadence continues to the next offset (a skip is not a stop — a temporarily disputed invoice that is later un-disputed resumes reminders at the next due offset).
- The final step is the natural point to surface a “this invoice is seriously overdue” signal to the org user for manual escalation, but the system does not itself take a legal/collections action.
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:
| Channel | Audience | Purpose | Carries |
|---|---|---|---|
| The 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-app | The 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.
| Condition | Internal status / signal | Behaviour |
|---|---|---|
| Paid in full | Settled (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 / void | Cancelled | Skip permanently — a cancelled invoice is not chased. |
| Refused by buyer | Refused (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). |
| Disputed | Disputed (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 issued | Draft / not finalized | No 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.
- The cadence offsets
D+7,D+14,D+30are exactly the overdue portion of the schedule. - Overdue is what the org-user push surfaces and what the final-step escalation flag is based on.
- Overdue does not change the AFNOR legal status. An invoice can be Acceptée (accepted by the buyer) and overdue at the same time — acceptance is a business decision; overdue is a timing fact.
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.
- The anchor date is the quote’s
expires_at(validity end). The default cadence fires as expiry nears (D-7,D-3,D-1before expiry), prompting the client to sign while the quote is still valid; like invoice cadences these offsets are defaults the customer may override (or switch off entirely) per quote before sending (§3.2). - Quote reminders skip when the quote is
Signed,Cancelled, or already expired. - The client-facing email carries the signing link rather than a payment link.
- An expired, unsigned quote is not chased; re-engagement is a manual re-issue as a new quote (4. Quote Domain §5.1).
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.
| Aspect | Invoice reminders | Quote reminders |
|---|---|---|
| Anchor date | due_at (payment due) | expires_at (validity end) |
| Goal | Get the invoice paid | Get the quote signed before expiry |
| Default cadence | D-3, D-day, D+7, D+14, D+30 | Pre-expiry offsets (e.g. D-7, D-3, D-1) |
| Skip on | Settled / Cancelled / Refused / Disputed | Signed / Cancelled / Expired |
| Client link in email | Payment 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:
- Per-document Temporal timers — when a remindable quote or invoice is sent with an automatic or custom cadence, a Temporal workflow schedules one timer per configured offset (relative to
due_at/expires_at). Each timer fires a reminder run that re-checks eligibility (§6) before sending. - Daily Temporal sweep over Postgres — a daily Temporal schedule scans remindable documents in Postgres for any offset that is due but not yet sent, acting as a safety net so a missed or delayed timer still results in exactly one reminder.
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
- Invariant: No reminder — automatic or manual — is ever sent after settlement. Once an invoice is
Settled(AFNOR Encaissée), the cadence is terminated and no run can send. - Invariant: A reminder never mutates the invoice’s AFNOR legal status or internal status. Reminders are side-effect-free on lifecycle state.
- Invariant: Reminders apply only to issued invoices (and finalized/sent quotes). A
Draftinvoice has no cadence. - Invariant: The amount asked for in a reminder is always the invoice’s full total. Invoices settle in full only; there is no partial-paid state and therefore no “remaining balance” a reminder could chase.
- Invariant: The reminder mode and cadence are chosen per document by the customer before sending (automatic / custom / none); the §4 offsets are overridable defaults.
- Invariant: The automatic cadence is bounded; after the last configured offset, no further automatic reminders fire. Continued chasing is a manual or out-of-band action.
- Invariant: Scheduling is owned by Temporal (per-document timers + a daily Postgres sweep); each cadence offset produces at most one reminder run per document, and runs are idempotent so neither path double-sends.
10. Related Documents
- 3. Invoice Domain — the issued-invoice model,
due_at, and the internal status set reminders read. - 4. Quote Domain — the quote model and
expires_atthat anchors quote reminders. - 6. Quote Signature — the signing flow a quote reminder drives toward.
- 8. Payment Link and Collection — the payment link embedded in invoice reminders.
- 9. Transaction Matching — the source of
Settled(full-amount match), the decisive skip condition. - PA 5. Lifecycle Statuses — the AFNOR legal status (Refusée / En litige / Encaissée) projected onto the invoice that gates reminders.
- PA 10. Integration Contracts —
TransmissionStatusChanged, the event that updates the projected status the reminder evaluator reads.
8. Payment Link and Collection
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:
- Payment link: a hosted, shareable means by which a client pays an issued invoice. It is tied to exactly one issued invoice and carries the amount, a structured reference, and the seller’s remittance details. Green-Got generates and hosts it.
- Collection (encaissement): the act of receiving payment for an issued invoice. In French VAT terms, encaissement is also the chargeable event under the collection regime — see PA 7. E-Reporting.
- Payment means: the EN 16931 structured fields describing how an invoice is to be paid (means code, IBAN, remittance/creditor reference). Defined canonically in PA 4. Formats and Invoice Data §4 (BG-16 / BT-81 / BT-84). Not redefined here.
- Remittance information / creditor reference: the structured reference a payer quotes on a bank transfer (and that Green-Got embeds in the payment means) so an inbound credit can be matched back to the invoice. See 9. Transaction Matching.
- Paid (AR-commercial) / Settled / Encaissée: the AR-commercial terminal status reached by collection is
Paid(driven byTransactionMatched);Settledis reserved for the AFNOR legal status Encaissée and the coarsebusiness_apiprojection (3. Invoice Domain §3). Either way the invoice is paid in full — an invoice is either unpaid or fullyPaid; there is no partial-paid state (§4). Reached only via Green-Got-owned collection, never by B2Brouter. - Bank transaction: an inbound credit observed on the seller’s Green-Got account, the event a collection is reconciled against in 9. Transaction Matching.
- PA / B2Brouter: the approved platform that transmits the invoice. It performs no payment processing — see §2.
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:
- present the client a way to pay (the payment link), and
- 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
| Item | Source | Purpose |
|---|---|---|
| Invoice number + issuer identity | The issued invoice (3. Invoice Domain) | Identifies what is being paid; shown to the payer. |
| Amount due | Invoice total | The 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. |
| Currency | Invoice currency (BT-5) | The settlement currency. |
| Structured payment reference | Green-Got-generated, embedded as the remittance/creditor reference | The key that lets an inbound bank credit be matched to this invoice (9. Transaction Matching). |
| IBAN / remittance details | The seller’s collection account | Where the money goes (when the rail is a transfer). |
| Due date | Invoice due_at | Shown 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 customer payment LINK is full-amount-only. There is no outstanding balance, no instalment, and no “partially paid” invoice status. A payment link is always for the full invoice amount: a payer who lacks the funds simply cannot pay a partial amount through it. An invoice reaches AR-commercial
Paidonly when a transaction covers its full total. “Partiellement payée” is not an invoice status. (AFNOR Approuvée partiellement (206) is partial approval of the invoice, unrelated to collection — see PA 5 §2 note.) - But the bank reality is still recorded. A real bank account collects whatever actually arrives — partials, overpayments, refunds, bank fees, manual rematches, and corrections. These are not discarded as model-less “anomalies”: they are recorded in the payment-allocation / VAT-collection ledger (§5.1), which is the substrate for VAT-on-collection reporting. The ledger records the bank truth; the AR-commercial
Paiddecision (full-amount only) is layered on top of it. A credit that does not settle the invoice still surfaces to the org user for handling and leaves its trace in the ledger — it is no longer treated as an event with no model.
The states a collection passes through:
| Stage | What happened | Invoice internal status |
|---|---|---|
| Link created | Payment link generated for the issued invoice (full amount). | Issued … (unchanged by link creation) |
| Client pays | The client pays the full amount through the link/rail. | Unchanged until the credit is observed. |
| Bank transaction arrives | An 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 amount | An 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:
- Settlement. Green-Got marks the invoice AR-commercial
Paidand emitsTransactionMatchedtoplateforme_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 toPaidonly; it is not the trigger for payment-data e-reporting. (Settledis reserved for the AFNOR legal status and the coarsebusiness_apiprojection — see 3. Invoice Domain §3.) - VAT collection. Green-Got records the real bank-level collection in the payment-allocation / VAT-collection ledger (§5.1) and emits
PaymentCollected.plateforme_agreeeconsumes it and — when VAT is due on collection (encaissement) — derives the payment-data overlay from that ledger entry:PaymentCollectedis 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). - Legal status. The AFNOR Encaissée (212) legal status, where it applies, is reached via
MarkOutboundInvoicePaid, never viaTransactionMatched. ThePaymentCollectedledger 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 enrichedEncaisséestatus 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 field | Purpose (referenced, not redefined) |
|---|---|
invoice_id | The 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_id | The real bank transaction observed on Green-Got rails. |
allocated_amount | The amount of this transaction allocated to this document — may be partial. |
vat_breakdown | List 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, source | When/how the collection occurred and where the record came from. |
correction_reversal_link | Links a correcting / reversing entry to the entry it amends (refunds, rematches). |
cumulative_collected_amount | Running 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):
- Default rail: SEPA Instant (SEPA Instant Credit Transfer). When the payer makes no other choice, the link settles the full amount instantly — the credit lands on the seller’s Green-Got account within seconds, carrying the structured reference for exact matching (9. Transaction Matching).
- Scheduled is offered: the payer may instead choose a scheduled transfer (e.g. on a future date), settling later without manual follow-up.
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:
- disable instant for a given invoice, payer, or amount (e.g. above a threshold, a suspect counterparty, a high-risk customer), falling back to a classic or scheduled SEPA transfer; or
- block the transfer entirely until Green-Got customer support reviews it, releasing it only after manual review.
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
- Invariant: A payment link is tied to exactly one issued invoice and carries the same structured payment reference as that invoice’s EN 16931 payment means (PA 4).
- Invariant: Settlement (
Settled/ Encaissée) is produced by Green-Got-owned matched payments only, never by B2Brouter. - Invariant: Creating/sharing a link does not change invoice status; only a full-amount matched inbound transaction drives the AR-commercial
Paidstatus. - Invariant: A link always collects the full invoice amount; invoices settle in full only, with no partial or outstanding-balance state.
- Invariant: A transaction that does not cover the full amount does not settle the invoice and is surfaced as an anomaly to the org user, never as a partial collection.
- Invariant: A settling match emits
TransactionMatched(settlement → AR-commercialPaid). The payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops) is derived fromPaymentCollected(the VAT-collection ledger entry), and only when VAT is on encaissement — never directly fromTransactionMatched, and never from the AFNOR Encaissée projection. - Invariant: The default rail is SEPA Instant; the payer may choose scheduled, and fraud/AML may disable instant or block the transfer pending Green-Got support review. The collection contract is rail- and timing-independent.
8. Related Documents
- 3. Invoice Domain — the issued invoice the link is tied to; amount, currency, due date.
- 7. Reminders — reminders embed the payment link to make dunning actionable.
- 9. Transaction Matching — how the inbound credit is reconciled to the invoice; full-amount match → AR-commercial
Paid, otherwise an anomaly. - bills 3. Payment and Matching — the AP-side rail pattern (SEPA Instant default, scheduled option, AML/fraud downgrade and blocking) this doc mirrors.
- 10. Tax and VAT — VAT-on-encaissement vs debits, which governs whether collection triggers the payment-data overlay (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops).
- PA 4. Formats and Invoice Data — the canonical EN 16931 payment-means fields the link mirrors.
- PA 5. Lifecycle Statuses — Encaissée and the fact that payment status is Green-Got-owned, not B2Brouter-owned.
- PA 7. E-Reporting — payment-data reporting triggered by confirmed collection (enriched 212/MEN for invoiced Flux-1 ops; Flux 10 only for non-invoiced ops).
- PA 10. Integration Contracts — the
TransactionMatchedevent contract.
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:
- Transaction matching: reconciling an inbound credit bank transaction observed on the seller’s Green-Got account to the issued invoice it pays. The invoicing-side (AR) counterpart of bills-side AP reconciliation.
- Inbound credit transaction: a credit movement on the seller’s Green-Got account — money arriving — that may correspond to a client paying an invoice. The matching subject.
- Match link: the persisted association between one issued invoice and one bank transaction, with the matched amount and the match source. The output of matching. See §8.
- Exact match: a deterministic match where the transaction’s remittance/reference contains the invoice’s structured payment reference (the same reference the payment link and the e-invoice carry — see 8. Payment Link and Collection).
- Fuzzy match: a heuristic candidate proposed to the org user when no exact reference is present — typically amount + counterparty + timing. Never auto-applied without confirmation. See §3.2.
- Anomaly: an inbound credit that references or plausibly relates to an invoice but does not cover its full amount. It does not settle the invoice; it is surfaced to the org user for manual handling. See §5.
- Paid (AR commercial) / Encaissée (AFNOR): the AR commercial terminal status is
Paid— invoice paid in full. An invoice is either unpaid orPaid; there is no partial state. The terminal outcome of a full-amount match. The corresponding AFNOR legal status Encaissée (presented asSettledon the legal/presentation axis) is a separate projection reflected back fromplateforme_agreee, never set by the match itself. TransactionMatched: the eventbus event invoicing emits toplateforme_agreeeon a full-amount match. It is the AR commercial match (issued invoice ↔ incoming bank transaction) and drives the AR commercial status toPaid. It does not by itself change the AFNOR legal status, and it is not the event that feeds payment-data reporting. The AFNOR Encaissée status is reached viaMarkOutboundInvoicePaid, never viaTransactionMatched. See the PA 10 event contract and §6.1.- Payment-collection event (
PaymentCollected): a separate event that records one real bank-level collection/allocation as an entry in the payment-allocation / VAT-collection ledger. It is the internal data source for payment-data (VAT-on-collection) reporting; the output carrier isEnriched212Menfor invoiced Flux-1 operations andFlux10only for non-invoiced operations (PA 7 §6). It does not by itself change the AFNOR legal status. The ledger is defined canonically in PA 10 §11 and appears in the schema at PA 12 §3. See §6.1. - Match source:
Auto(system-applied exact match) orManual(user-confirmed, including accepted fuzzy proposals and manual overrides).
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:
- a transaction may quote a clean reference, a partial reference, or none;
- the same amount may plausibly match several open invoices for the same counterparty;
- a credit may arrive that does not cover an invoice’s full amount, which can never settle it (invoices settle in full only) and must be surfaced as an anomaly rather than silently absorbed.
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
| Strategy | Key | Confidence | Auto-applied? |
|---|---|---|---|
| Exact (reference) | Transaction remittance/reference contains the invoice’s structured payment reference | High — deterministic | Yes (source = Auto) when it resolves to exactly one invoice |
| Fuzzy (amount + counterparty) | Full invoice amount + counterparty identity + timing window | Lower — heuristic | No — proposed to the org user, applied as source = Manual on confirmation |
| Manual | User explicitly links a transaction to an invoice | Authoritative | Applied 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.
- Full-amount match → the invoice becomes
Paid(the AR commercial status). This is the only matching outcome that settles an invoice. The match emitsTransactionMatched; the corresponding AFNOR Encaissée legal status, where it applies, is a separate projection reflected back fromplateforme_agreee, not a direct side effect of the match (see §6). - Transaction that does not cover the full amount: it never settles the invoice and never produces a settling match link. Rather than recording a partial commercial settlement or accruing a commercial balance, the system surfaces the credit as an anomaly to the org user for manual handling (the money arrived but does not correspond to a settleable full payment). The org user investigates and resolves it out of band.
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:
- 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. - invoicing emits
TransactionMatchedtoplateforme_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. - The corresponding AFNOR Encaissée (212) legal status, where it applies, is reflected back as a projection from
plateforme_agreeeviaTransmissionStatusChanged(3. Invoice Domain §6).TransactionMatcheddoes 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.
| Aspect | TransactionMatched (commercial match) | PaymentCollected (payment collection / allocation) |
|---|---|---|
| What it represents | The 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 drives | The 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 written | The match link (§8). | One entry in the payment-allocation / VAT-collection ledger (PA 10 §11, PA 12 §3). |
| Amount semantics | Always 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 status | None 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_state — not 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:
- Confirm a fuzzy proposal that covers the full amount → creates a
Manualmatch link and settles the invoice. - Manually link a specific transaction to a specific invoice →
Manualmatch link, even when no automatic candidate was offered. It settles the invoice only if it covers the full amount. - Unmatch / re-match an incorrect match → removes the match link and reverts the invoice from
Paidto its prior commercial status.
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:
- link the credit to an invoice as a
Manualmatch — settles only if it covers the net target (§5); - dismiss / mark-handled (e.g. it was a bank fee, a non-invoice credit, or handled out of band), which clears it from the surface and records the resolution;
- flag for refund / further investigation, routing it to the appropriate operations follow-up.
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.
| Field | Type | Meaning |
|---|---|---|
id | prefixed time_sortable_id | Match-link identifier. |
invoice_id | issued-invoice id (inv_…) | The AR invoice being settled. |
transaction_id | bank-transaction id | The inbound credit transaction (Green-Got rails / core_banking). |
matched_amount | integer cents (i64) | The settled amount; equals the invoice’s full total (a match links only on the full amount). |
currency | ISO 4217 | Currency of the matched amount; must equal the invoice currency for a settling match (see 10. Tax and VAT). |
source | enum Auto | Manual | How the match was made — Auto for exact reference, Manual for confirmed-fuzzy and manual links/overrides. |
collection_date | date | The date the payment was collected; carried in TransactionMatched and used for payment-data reporting. |
created_at | timestamp | When 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
- Invariant: AR transaction matching reconciles inbound credits to issued invoices only; it is separate from bills/AP reconciliation and shares no table with it.
- Invariant: Exact matches (
Auto) require a reference resolving to exactly one invoice; ambiguity downgrades to a fuzzy proposal, never an automatic settlement. - Invariant: Invoices settle commercially in full only; AR
Paidis reached only by a transaction covering the invoice’s full amount via the link. There is no partial, instalment, or outstanding-balance commercial state. - Invariant: A transaction that does not cover the full amount never settles the invoice commercially and produces no settling match link; it is surfaced as an anomaly to the org user, not recorded as a partial commercial settlement. (Real bank-level collection, including partials, is recorded separately in the payment-allocation ledger — §6.1.)
- Invariant: A settled invoice has exactly one settling match link covering its full total; a full-amount match emits
TransactionMatched, and an override that changes the outcome emits the corresponding correction. - Invariant:
TransactionMatched(commercial match →Paid) andPaymentCollected(bank-level collection → ledger → payment data) are distinct events; neither changes the AFNOR legal status by itself. The AR commercialPaidstatus is set by invoicing, never received from B2Brouter; payment data follows only the encaissement regime and is fed byPaymentCollected, not byTransactionMatched— carried asEnriched212Menfor invoiced Flux-1 operations andFlux10only for non-invoiced (PA 7 §6).
10. Related Documents
- 3. Invoice Domain — the issued invoice, its total, and the internal status set matching drives.
- 7. Reminders —
Paid(full-amount match) is the decisive skip condition for reminders. - 8. Payment Link and Collection — the full-amount payment link and structured reference that make exact matching deterministic; the SEPA-Instant-default rail; the collection lifecycle this doc completes.
- 10. Tax and VAT — VAT-on-encaissement (which gates payment-data reporting) and multi-currency notes for cross-border collection.
- PA 5. Lifecycle Statuses — the reflected Encaissée (212) legal status, distinct from the AR commercial
Paid. - PA 7. E-Reporting — payment-data reporting, fed by the payment-collection event (
PaymentCollected), not byTransactionMatched; carried asEnriched212Menfor invoiced Flux-1 operations andFlux10only for non-invoiced (§6). - PA 10. Integration Contracts — the authoritative
TransactionMatchedandPaymentCollectedevent contracts (§6, §7) and the payment-allocation / VAT-collection ledger (§11). - PA 12. Data Model — where the payment-allocation ledger entry sits in the consolidated schema (§3).
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.
- Class:
staging-wire· Owner: Claude (research) → Backend / Integration · Due: Before outbound go-live · Launch-blocking: yes - Evidence: 2. Dual Invoice Model §3.4, PA 4. Formats §4
- Status: RESEARCH PENDING (Claude)
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?
- Class:
legal/staging-wire· Owner: Claude (research) → Backend + Compliance · Due: Before outbound go-live · Launch-blocking: yes - Evidence: 5. Numbering, PA 6. Annuaire and Routing
- Status: RESEARCH PENDING (Claude)
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.
- ✅ Ported (2026-06-22): 4. Quote Domain §5.1
has been rewritten to this decision (expired = non-signable and non-convertible; only forward action
is “re-issue as a new quote”), and the quote state machine + key-characteristics + §8 validity-expiry invariant
updated accordingly (the prior “direct conversion / late acceptance” path is removed). The §5.1 anchor changed
from
#51-late-acceptance-of-an-expired-quoteto#51-expired-quote--re-issue-not-late-acceptance. - Class:
product-decision· Owner: Product / Backend · Launch-blocking: no - Port to: 4. Quote Domain §5.1 + quote lifecycle/state docs — done.
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.
- Class:
product-decision(waspost-MVP) · Owner: Backend · Launch-blocking: no (but on the critical build path) - Port to: 2. Dual Invoice Model §3.3, 3. Invoice Domain §3.1
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_agreee/uncertainties.md — the transport-layer active register (B2Brouter wire values, status/format detail).
- 2. Dual Invoice Model — classical vs structured model, BT-21 legal notes.
- 4. Quote Domain — quote lifecycle, expiry, re-issue.
- 5. Numbering — gap-free numbering and identifier format.
- PA 14. Implementation Wiring — how AR wires into transport.
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
- 1. Reform Overview — the French reform, rollout calendar, e-invoicing vs e-reporting, Green-Got’s role, and the canonical glossary referenced by every other doc.
- 2. Platform Architecture — the post-2024 five-corner model: PPF (directory + concentrator), PA, OD/SC, annuaire, Flux 1 vs Flux 10, Peppol.
- 3. Actors and Legal Posture — who the parties are, the mandate/representation model, and the liability matrix.
Invoice model & lifecycle
- 4. Formats and Invoice Data — Factur-X / UBL / CII, EN 16931, mandatory mentions, VAT category codes, document type codes. The canonical structured-invoice contract.
- 5. Lifecycle Statuses — the three status layers (AFNOR XP Z12-012 legal ↔ B2Brouter API ↔ Green-Got internal) and the canonical mapping table. Cornerstone doc.
- 6. Annuaire and Routing — SIREN/SIRET routing, the national annuaire vs B2Brouter’s directory, recipient-PA resolution.
Compliance
- 7. E-Reporting — Flux 10: B2C, cross-border B2B, and payment data; cadence, deadlines, and the B2Brouter-automatic vs Green-Got-supplied split.
- 8. Archiving and Audit — legal archive model (store the exact transmitted/received artefact + hash + provider metadata; regeneration is display-only), retention, piste d’audit fiable, SAE, and who archives.
- 13. Privacy and Data Protection — the privacy appendix: data categories, lawful basis, legal-archive vs operational/PII retention, DSAR/erasure exceptions, log masking, and B2Brouter DPA/subprocessor responsibilities.
Onboarding & contracts
- 9. Mandate and Onboarding — enrolling a customer: mandate UI → B2Brouter account (eDocSync) → DGFiP tax report settings → annuaire registration.
- 10. Integration Contracts — the eventbus + data contracts between
invoicing,plateforme_agreee, andbills. Resolves the inbound→billsownership and the canonical event names.
Architecture
- 11. European Extension — the target multi-country architecture: a shared EN 16931 core + per-country packs, with France as the first pack. How the canonical-model-plus-mappers design generalizes across the EU.
- 12. Data Model — the consolidated entity-relationship schema across organisation / invoicing / bills / plateforme_agreee: Organisation vs Client vs Supplier, where B2Brouter sits, and the full FK map. Includes the direction-aware
PaTransmission, the payment-allocation / VAT-collection ledger, and theComplianceEventLog. - 14. Implementation Wiring — the cross-crate wiring plan for the implementing agent: crates, stores, use cases, adapters, Temporal workflows, eventbus rules, routers, feature flags, and command/event ownership, with onboarding/enrollment as a prerequisite phase.
B2Brouter API reference
- B2Brouter — Documentation Index — the concrete API surface (auth, accounts, sending, receiving, statuses/webhooks, mechanics).
Other
- Uncertainties — the active, classified register of open legal / provider-support / staging-wire / product-decision / post-MVP items, each with owner, milestone, launch-blocking flag, and evidence link.
To Be Created
- A worked end-to-end example (issue → transmit → status → settle → e-report) once the crate is implemented.
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.
- PPF (Portail Public de Facturation): the French State public invoicing portal. After the 2024 architectural revision it acts only as the central Annuaire (directory) and as the data concentrator that aggregates invoice data, lifecycle statuses, and e-reporting for DGFiP. It no longer sends or receives invoices, and companies cannot use it directly.
- PA (Plateforme Agréée / Approved Platform): a State-registered, certified third-party platform that is the sole authorized intermediary for invoice exchange and e-reporting. Formerly called PDP (Plateforme de Dématérialisation Partenaire); the terminology changed from PDP to PA in July 2025. B2Brouter is a certified PA.
- OD / SC (Opérateur de Dématérialisation / Solution Compatible): an optional, non-certified software operator (ERP, accounting, or invoicing SaaS). The terminology changed from OD to Solution Compatible (SC) in July 2025. An SC cannot send or receive invoices directly; it must operate through a PA. Green-Got is an SC, layered on top of B2Brouter (the PA).
- DGFiP (Direction Générale des Finances Publiques): the French tax administration. It registers approved platforms, runs conformance testing, and is the ultimate recipient of invoice and e-reporting data through the PPF concentrator. As of 2025-07-08 it is also the French Peppol national authority.
- SIREN: the 9-digit identifier of a French legal entity. It is the primary routing key in the Annuaire.
- SIRET: the 14-digit identifier of a specific establishment of a legal entity (SIREN + 5-digit NIC). Used for establishment-level routing.
- EN 16931: the European standard defining the semantic data model for an electronic invoice. All formats accepted by the reform comply with it.
- Factur-X: a hybrid invoice format — a PDF/A-3 (ISO 19005-3) human-readable document with an embedded
factur-x.xmlXML payload (CII D22B syntax — the reform version, EN 16931; the older D16B baseline is not the reform syntax). Equivalent to the German ZUGFeRD 2.x. A single file is both printable and machine-readable. - UBL (Universal Business Language): an OASIS XML invoice syntax. The reform accepts UBL 2.1. Pure XML, machine-readable only.
- CII (Cross Industry Invoice): the UN/CEFACT XML invoice syntax. Pure XML, machine-readable only; it is also the XML embedded inside Factur-X. Invariant: all reform CII / Factur-X syntax is D22B (EN 16931); the older D16B baseline is not the reform syntax.
- Flux 1: the data flow for domestic B2B e-invoicing — structured invoices exchanged between French taxable persons (assujettis) through approved platforms.
- Flux 10: the data flow for e-reporting — transmission of B2C and cross-border transaction data and payment data to DGFiP.
- Données de paiement (payment data): collection-event metadata (date, amount incl. VAT, VAT per rate, currency) transmitted under CGI art. 290 A whenever VAT becomes due on collection (encaissement). This obligation is independent of the Flux 1 / Flux 10 transaction channel — it can apply on top of a domestic B2B e-invoice (typically a service invoice). See 7. E-Reporting.
- Franchise en base: the French VAT-exemption regime for small turnovers. A company in franchise en base is not VAT-liable but remains an assujetti and is therefore in scope for e-invoicing and e-reporting.
- Annuaire: the central directory hosted by the PPF. Pre-filled from INSEE/SIRENE (~4.5M companies), it holds each company’s SIREN, SIRET(s), chosen PA, and optional internal routing code. It is the routing source of truth.
- CDAR (Compte-Rendu d’Acceptation/Rejet): PPF delivery receipts/acknowledgements; status updates that flow back to the sender through its PA.
- PAF (Piste d’Audit Fiable / reliable audit trail): an unbroken evidence chain linking each invoice to its underlying transaction, with timestamps, actors, data changes, and integrity checks.
- SAE (Système d’Archivage Électronique): a certified electronic archiving system (per NF Z42-013 / NF Z42-029) used as one legal method of guaranteeing invoice integrity and retention.
- eIDAS: the EU regulation on electronic identification and trust services; basis for the QES (qualified electronic signature) integrity method.
- B2B (Business-to-Business): transactions between assujettis (taxable persons), regardless of VAT liability. Domestic B2B is in scope for e-invoicing.
- B2C (Business-to-Consumer): transactions with non-taxable persons. Out of e-invoicing scope but subject to e-reporting.
- B2G (Business-to-Government): transactions with public administration. Routed via Chorus Pro.
- AFNOR XP Z12-012: the French standard (February 2026 edition — version 1.3, published 2026-02-25, in force — replaces the November 2025 edition) defining the formats/profiles and the invoice lifecycle statuses transmitted to the PPF. The Feb-2026/v1.3 edition introduces Flux 11 (built on Flux 10). The earlier lineage (a May-2025 edition, then an update published 2025-07-31, then the November-2025 edition) is reported by secondary sources but the precise predecessor sequence is not pinned at the primary AFNOR source and must be confirmed at the source-refresh gate (§7). See 5. Lifecycle Statuses. Source: AFNOR boutique XP Z12-012 (Norm’Info, publication 2026-02-25).
- Chorus Pro: the legacy French B2G portal for invoices addressed to public administration.
- Peppol: an international interoperability network for electronic procurement and invoicing (UBL/CII). France became a Peppol national authority on 2025-07-08.
- assujetti: a taxable person carrying out an economic activity, identified by SIREN/SIRET. Being an assujetti does not require being VAT-liable — a company in franchise en base is an assujetti and is in scope. The issuing company is the assujetti for its sales.
- avoir: a credit note — a corrective invoice (document type code 381) issued to correct or cancel a finalized invoice.
- mandat: the legal authorization by which a company empowers a PA to act on its behalf (issue, receive, e-report, manage statuses). The company remains legally responsible for invoice content; the PA is responsible for transmission, format, and audit trail. See 3. Actors and Legal Posture.
- ETI (Entreprise de Taille Intermédiaire): intermediate-sized enterprise (250–5,000 employees).
- PME: small and medium enterprise (< 250 employees).
- micro: micro-enterprise (< 10 employees or < €2M turnover).
- INSEE / SIRENE: the French national statistics institute and its business register, the source of pre-population for the Annuaire.
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:
- E-invoicing (facturation électronique) — mandatory exchange of structured domestic B2B invoices exclusively through an approved platform (PA).
- E-reporting (transmission de données / e-reporting) — periodic transmission of transaction and payment metadata to DGFiP for the transactions that fall outside domestic B2B e-invoicing.
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:
- Franchise en base and micro-enterprises are in scope even though they charge no VAT. They are assujettis, so they must be able to receive e-invoices from 2026-09-01 and must issue e-invoices and perform e-reporting from their phase date.
- A company that “facture sans TVA” (issues invoices without VAT) is still concerned: the obligation attaches to the assujetti, not to the VAT line.
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)
- Applies to taxable supplies of goods and services between two French taxable persons (assujettis), identified by SIREN/SIRET, including advance payments and public auction sales. Both parties being assujettis established in France is what places the operation in the domestic e-invoicing channel — VAT liability of either party is not the test.
- Invoices must be issued, transmitted, and received as structured documents in one of three formats — Factur-X, UBL 2.1, or CII — all compliant with EN 16931. See 4. Invoice Formats and Mandatory Data.
- The exchange flows through approved platforms over Flux 1 and is tracked through a standardized lifecycle. See 5. Lifecycle Statuses.
3.2 E-Reporting (B2C and cross-border)
- Covers transactions outside domestic B2B: B2C transactions, cross-border B2B (intra-EU and non-EU), and payment data (collection date and amount, used to determine when VAT becomes due on collection/encaissement).
- Only transaction and payment metadata is transmitted to DGFiP over Flux 10 — no structured invoice is exchanged between parties.
- Cadence is set by the company’s VAT regime (and the current legal text), not by company size. See 7. E-Reporting for the per-regime cadence.
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:
- Domestic B2B e-invoicing (Flux 1) — both parties are French assujettis: a structured invoice is exchanged.
- B2C & cross-border e-reporting (Flux 10) — sale to a consumer, or any operation with a counterparty outside France: only transaction metadata is reported.
- Out of scope — e.g. B2G (routed via Chorus Pro), or operations outside the reform entirely.
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 type | Channel (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
- Phase 1 — 2026-09-01: Large enterprises (> 5,000 employees or turnover > €1.5B) and ETI (250–5,000 employees) must issue e-invoices, receive e-invoices, and perform e-reporting.
- Phase 2 — 2027-09-01: PME (< 250 employees) and micro enterprises (< 10 employees or < €2M turnover) must issue e-invoices and perform e-reporting. These companies are already obliged to receive since 2026-09-01.
Key characteristics:
- Reception is universal from 2026-09-01. Every in-scope French assujetti (every Annuaire-registered entity) — regardless of size, and including franchise-en-base / micro that have no VAT number — must be able to receive e-invoices from that date, because any large/ETI supplier may issue an e-invoice to them.
- Issuance is phased by company size as above.
- A voluntary pilot window runs from approximately 2026-02-28 to 2026-08-31, allowing companies and platforms to test before the obligation begins.
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
| Milestone | Date | Who | Issue | Receive | E-reporting |
|---|---|---|---|---|---|
| Pilot window | ~2026-02-28 → 2026-08-31 | Voluntary participants | Optional | Optional | Optional |
| Phase 1 | 2026-09-01 | Large (>5,000 emp or >€1.5B) + ETI (250–5,000) | Mandatory | Mandatory | Mandatory |
| Universal reception | 2026-09-01 | All in-scope French assujettis (Annuaire-registered) | — | Mandatory | — |
| Phase 2 | 2027-09-01 | PME (<250) + micro (<10 emp or <€2M) | Mandatory | Mandatory (since 2026) | Mandatory |
| Non-established sellers (large/ETI) | 2026-09-01 | Foreign taxpayers (no FR PE) selling goods/services | n/a | n/a | Mandatory (e-reporting) |
| Non-established sellers (micro/VSE/SME) + VAT-liable buyers (any size) | 2027-09-01 | Foreign taxpayers (no FR PE): smaller sellers; all sizes as buyers liable for FR VAT (reverse charge, non-exempt intra-EU acquisitions) | n/a | n/a | Mandatory (e-reporting) |
| Grace period | ~ to Jan 2027 or longer | Good-faith effort | No “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:
- Building the structured e-invoice (the dual internal/EN 16931 model) before submission.
- Submitting invoices to B2Brouter and surfacing their legal lifecycle status to the customer.
- Receiving inbound supplier invoices via B2Brouter and routing them into the accounts-payable domain.
- Onboarding the customer onto the PA and capturing the mandat. See 9. Onboarding.
- Generating payment links on Green-Got’s own rails (B2Brouter performs no payment processing).
5.1 What Green-Got’s customers must do
- Be registered in the Annuaire with their chosen PA (B2Brouter, provisioned through Green-Got) before 2026-09-01, declaring routing scope.
- Be able to receive e-invoices from 2026-09-01.
- Issue structured e-invoices from their applicable phase date (2026-09-01 for large/ETI, 2027-09-01 for PME/micro).
- Perform e-reporting for B2C and cross-border transactions from their applicable phase date.
- Remain legally responsible for the content and compliance of their invoices and for archiving. See 3. Actors and Legal Posture and 8. Archiving.
6. Related Documents
- 2. Platform Architecture — the 5-corner model and routing.
- 3. Actors and Legal Posture — parties, responsibilities, and the mandat.
- 4. Invoice Formats and Mandatory Data — Factur-X / UBL / CII and EN 16931 fields.
- 5. Lifecycle Statuses — AFNOR XP Z12-012 statuses.
- 7. E-Reporting — Flux 10 obligations.
- 8. Archiving — retention and audit trail.
- 9. Onboarding — PA registration and mandate capture.
7. Sources
- French e-invoicing reform overview and mandatory mentions: https://entreprendre.service-public.gouv.fr/actualites/A15683
- CGI art. 289 bis / 290 / 290 A (e-invoicing scope for assujettis established/domiciled/resident in France; e-reporting; payment-data obligation; reverse-charge exclusion) — current Legifrance pages in force since 2026-02-21 (article ids LEGIARTI000053546660 art. 289 bis / LEGIARTI000053546668 art. 290 / LEGIARTI000053546674 art. 290 A); note the CIBS recodification effective 2026-09-01: https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000053546674
- Source-refresh gate — LAUNCH-BLOCKING: before any live deploy, a tracked source-refresh task must re-verify (a) the AFNOR XP Z12-012 edition (February 2026 / v1.3 in force) and its predecessor lineage, and (b) the CGI art. 289 bis / 290 / 290 A Legifrance article ids/dates above against the live sources. The CIBS recodification (Ordinance 2025-1247 of 2025-12-17, effective 2026-09-01) moves the VAT rules from the CGI into Book II of the Code des impositions sur les biens et services (CIBS) — ~230 CGI articles become ~1,000 CIBS articles, so 289 bis / 290 / 290 A will be renumbered. Every CGI article id cited across this doc set is therefore a moving target and must not be assumed stable at launch. This is a launch-blocking item, not a best-effort note: do not deploy until the CIBS/CGI article ids are re-verified against the in-force code. Sources: PwC — recodification TVA dans le CIBS; KPMG Avocats — CIBS et TVA 2026.
[open — source-refresh — launch-blocking] - Sanctions and scope (assujettis established in France): https://entreprendre.service-public.gouv.fr/actualites/A18802?lang=fr
- Franchise en base / micro in scope despite no VAT liability: https://www.impots.gouv.fr/professionnel/questions/franchise-en-base-micro-entrepreneur-ou-auto-entrepreneur-suis-je-concerne
- Rollout calendar and preparation updates: https://www.bdo.global/en-gb/insights/tax/indirect-tax/france-mandatory-e-invoicing-to-be-implemented-in-2026-updates-on-preparations
- 2026/2027 mandate detail: https://www.avalara.com/blog/en/europe/2025/09/france-e-invoicing-e-reporting-mandate-2026-2027.html
- September 2026 simplification measures: https://www.ey.com/en_gl/technical/tax-alerts/french-government-announces-simplification-measures-as-part-of-september-2026-e-invoicing-mandate
- Grace period (no official delay, June 2026): https://www.vatupdate.com/2026/06/02/france-e-invoicing-no-official-delay-but-a-grace-period-until-january-2027-or-longer-is-taking-shape/
- E-invoicing vs e-reporting: https://ecosio.com/en/blog/e-invoicing-and-e-reporting-in-france/
- E-reporting scope: https://www.cleartax.com/fr/en/e-reporting-france
- E-reporting launch for B2C and cross-border: https://www.vatupdate.com/2025/10/22/france-launches-e-reporting-for-b2c-and-cross-border-b2b-transactions-starting-september-2026/
- DGFiP approved platforms page: https://www.impots.gouv.fr/facturation-electronique-et-plateformes-agreees
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:
- AR (Accounts Receivable): invoices a Green-Got customer issues to their own clients. Owned by the
invoicingcrate. - AP (Accounts Payable): invoices a Green-Got customer receives from their suppliers. Owned by the
billscrate. - Transmission: a
plateforme_agreeerecord representing one regulated exchange through B2Brouter — either an outbound submission of an issued invoice or the inbound receipt of a supplier invoice. The transport-layer counterpart of an AR invoice or an AP bill. - Outbound contract: the chain by which an
invoicingissued invoice becomes a transmitted, legally-tracked e-invoice (invoicing→plateforme_agreee→ B2Brouter → recipient PA) and its legal lifecycle status flows back. - Inbound contract: the chain by which a supplier e-invoice arriving through B2Brouter becomes an AP
Bill(B2Brouter →plateforme_agreee→bills). - Canonical structured invoice: the EN 16931 structured data model defined in 4. Formats and Invoice Data. It is the handoff type between
invoicingandplateforme_agreeefor outbound. Not redefined here — referenced. - Legal lifecycle status: the AFNOR XP Z12-012 status set (4 mandatory + recommended) defined in 5. Lifecycle Statuses. Distinct from B2Brouter API statuses and from Green-Got’s internal projection.
- Projection: a read-model copy of a status or fact, owned by a consuming crate, kept up to date by events.
invoicingholds a projection of the legal lifecycle status; it does not own that status. - EVENT_BUS: per the crate architecture, each crate owns a static typed
EventBus<CrateEvent>; other crates’rules/subscribe to it (including cross-crate). See 9. Mandate and Onboarding for where the enrollment link lives.
2. The Three-Crate Responsibility Split
| Crate | Owns | Does 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
invoicingfinalizes 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).invoicingemitsInvoiceFinalizedon its EVENT_BUS, carrying the invoice id and enough reference forplateforme_agreeeto assemble the canonical structured invoice.plateforme_agreee(subscribed via arules/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-levelb2brouter_account_id(resolved from the enrollment — see 9. Mandate and Onboarding §7).- 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.
- Legal lifecycle statuses (AFNOR XP Z12-012: Déposée, Rejetée, Refusée, Encaissée, plus recommended intermediates) flow back to
plateforme_agreeevia B2Brouter webhooks / CDAR.plateforme_agreeeowns the authoritative status on the transmission. See 5. Lifecycle Statuses. plateforme_agreeeemitsTransmissionStatusChangedon each legal-status transition.invoicingconsumes 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:
- The canonical structured invoice handoff type is defined in 4. Formats and Invoice Data — it is not redefined here.
invoicingproduces the data;plateforme_agreeemaps it to B2Brouter JSON; B2Brouter produces the XML. plateforme_agreeeresolves the organisation-levelb2brouter_account_idfrom the enrollment per submission; the account id is never copied onto the invoice or the transmission as a source of truth.- Status flows one direction for authority (PA → invoicing) and
invoicingonly ever projects.
4. The Inbound Contract (transport → AP bill)
4.1 Flow
- 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).
plateforme_agreeelearns of the arrival. Polling is the authoritative inbound channel:plateforme_agreeepollsGET /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.plateforme_agreeepersists an inbound transmission record and downloads the legal original document. Structured metadata/fields come fromGET /invoices/{id}; the legal/original bytes that are archived (the supplier’s Factur-X / UBL / CII artefact) are fetched viaGET /invoices/{id}/as/original. Anyattachments[]are annexes / supplemental only — they are NEVER the legal invoice and must not be treated as the archived original.plateforme_agreeeemitsInboundInvoiceReceivedon its EVENT_BUS, carrying the transmission id, the organisation id, references to the downloaded document, and the extracted structured fields.bills(subscribed via arules/handler) consumesInboundInvoiceReceivedand creates the APBillin theReceivedstate. Because PA-delivered invoices arrive with structured data, they need no OCR — but “structured source / no OCR needed” is not aBill.status; it is recorded as extraction metadata outsideBill.status(asource/extractionfield on theBill). TheBillreferences theplateforme_agreeetransmission (for the original document and legal trail), and the supplier master, but is owned bybills.
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. TheInboundInvoiceReceivedevent carries adocument_refpointing at the stored object, not the bytes;document_vaultgoverns the retention horizon over it. Issued (outbound) invoices are likewise stored — Green-Got fetches the exact transmitted legal artefact (the PA-generated file viadownload_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:
- Inbound creates an AP
Bill— never aninvoicing.invoice. - The legal original is fetched via
GET /invoices/{id}/as/original(the archived bytes) and structured fields viaGET /invoices/{id}.attachments[]are annexes / supplemental only and are never the legal invoice. - PA-delivered invoices arrive structured;
billsskips OCR and lands them inReceived. The “structured source / no OCR” fact is extraction metadata (asource/extractionfield on theBill), not aBill.statusvalue — there is noExtractedbill status. (Email/upload channels still OCR; those arebills-only channels and do not involveplateforme_agreee.) - The buyer’s accept/refuse business decision on a received invoice is a
billsconcern that may surface back as a legal status to the supplier via B2Broutermark_as(accepted/refused) — but that status-back is mediated byplateforme_agreeeon the transmission, not authored byinvoicing.
Design rule — inbound (received) status involves four separate axes. A received French invoice has four distinct states, never one enum:
| Axis | Owner | Notes |
|---|---|---|
| Green-Got bill payment state | bills (AP) | whether Green-Got has scheduled/made payment of the supplier bill on its own rails. |
| Supplier-invoice acceptance/refusal state | bills (AP business decision) | the buyer decision (accepted / refused) Green-Got makes on the received document. |
| B2Brouter local state | plateforme_agreee (PA adapter) | the PA’s operational state of the received object. |
| PPF / AFNOR status | PPF 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.
| Event | Direction (producer → consumer) | Producer | Consumer(s) | Payload summary | Purpose |
|---|---|---|---|---|---|
InvoiceFinalized | invoicing → plateforme_agreee | invoicing | plateforme_agreee | invoice_id, organisation_id, structured-invoice reference (per doc 4) | An immutable AR invoice is ready for regulated transmission; PA begins the outbound contract. |
TransmissionStatusChanged | plateforme_agreee → invoicing | plateforme_agreee | invoicing | transmission_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 reason | An authoritative legal lifecycle status changed; AR invoice updates its projection. |
InboundInvoiceReceived | plateforme_agreee → bills | plateforme_agreee | bills | transmission_id, organisation_id, document_ref, extracted_fields (structured supplier-invoice data), received_at | A supplier e-invoice arrived and was persisted+downloaded; AP creates a Bill. |
TransactionMatched | invoicing → plateforme_agreee | invoicing (AR matching) | plateforme_agreee | invoice_id, transmission_id, matched amount (full invoice total), currency, collection date | A 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. |
PaymentCollected | invoicing → plateforme_agreee | invoicing (collection/allocation) | plateforme_agreee | one 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. |
MarkOutboundInvoicePaid | command (within plateforme_agreee) | plateforme_agreee | plateforme_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. |
ReportableAcquisitionRecorded | bills → plateforme_agreee | bills (AP) | plateforme_agreee | bill_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 state | AP 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:
- Allocation-ledger write first.
invoicingappends the payment-allocation ledger entry (with its breakdown groups) in the same DB transaction as its ComplianceEventLog record. - Then
PaymentCollected. The ledger write emitsPaymentCollected, carrying the allocation group id(s) / a deterministic correlation id. - Then
MarkOutboundInvoicePaid. The 212/Encaissée push is sequenced after the ledger entry exists and references it byallocation_correlation_id(orplateforme_agreeereads 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:
| # | Axis | Owner / source of truth | What it answers |
|---|---|---|---|
| 1 | Invoice commercial status (AR) | invoicing | Where 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.) |
| 2 | Outstanding balance | invoicing | How much of the invoice total is still owed (a monetary amount, not a status)? |
| 3 | VAT collection event | the 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. |
| 4 | B2Brouter state | plateforme_agreee (PA adapter) | The PA’s operational transmission state (new/sent/accepted/…). |
| 5 | AFNOR / CDAR lifecycle status | PPF 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:
- Settlement (AR lifecycle).
invoicingmatches an incoming bank transaction that pays the link in full and emitsTransactionMatched.plateforme_agreeedoes not treat this as the payment-data trigger;invoicingdrives the AR commercial status toPaid(the commercial-axis terminal — the AFNOR-legalSettled/Encaissée is a separate axis). The sameTransactionMatchedalso triggers the explicitMarkOutboundInvoicePaidcommand (§6) — the AFNOR 212 (Encaissée) lifecycle push to the recipient’s PA when legally applicable (CGI art. 290 A / VAT-on-collection) — and/or thePaymentCollectedVAT-collection ledger entry below. Ordering (so the MEN can be correlated): the allocation-ledger write and itsPaymentCollectedemission happen first, andMarkOutboundInvoicePaidis sequenced after them, referencing the same allocation group id(s) / deterministic correlation id (see the ordering invariant in §6 and 14. Implementation Wiring §3).MarkOutboundInvoicePaid(legal lifecycle status) andPaymentCollected(Flux 10 ledger) are distinct legal projections of the same bank collection and must not be conflated. - VAT collection (reporting). For VAT-on-collection, the platform must model real bank-level collection even though the link is full-amount-only — e.g. an invoice partly paid by a separate bank transfer outside the link, a correction, or a reversal. Each such collection is recorded as one payment-allocation ledger entry (§11) and emitted as
PaymentCollected. plateforme_agreeeconsumesPaymentCollected(the ledger is the data source) and ensures the payment data (collection date, allocated amount incl. VAT, VAT amount by rate, currency if not EUR) is e-reported when VAT is on collection. The channel depends on whether the operation was invoiced: for invoiced Flux-1 operations (domestic B2B e-invoices) the payment data feeds the enriched AFNOR 212 / Encaissée status (the MEN) — thePaymentCollectedledger is the source but the legally-effective output is the enriched 212, not a separate submission; a separate Flux 10 payment-data flow applies only to non-invoiced operations (B2C, international B2B). B2Brouter performs the transmission automatically for a DGFiP-enabled account; no separate Green-Got call to DGFiP is made. The exact B2Brouter wire (whether B2Brouter derives the enriched 212/MEN or Green-Got supplies the MEN) is gated on staging verification (L-4 / P-7). See 5. Lifecycle Statuses §13 and 7. E-Reporting.
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:
- The current account (axis: “which account do we transmit through now”) is organisation-level. Outbound and inbound flows resolve it from the enrollment at the time of the B2Brouter call. It is mutable (an org can be migrated to a new account / a new PA).
- The account that actually carried a given transmission (axis: “which account did this exchange go through”) is persisted on the
PaTransmissionasb2brouter_account_id— an immutable historical snapshot, captured at submission/receipt time. It is not a mutable FK that follows the org’s current account.
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:
| Field | Type / target | Notes |
|---|---|---|
id | PK (pat_) | transmission id. |
direction | enum outbound | inbound | the axis that decides which document reference is set. |
invoice_id? | nullable FK → invoicing.invoice | set only when direction = outbound; the issued AR invoice. NULL for inbound. |
bill_id? | nullable FK → bills.bill | the inbound AP document/bill created from this transmission; set only when direction = inbound. NULL for outbound. |
organisation_id | FK → Organisation / enrollment | resolves the current B2Brouter account; the enrollment link. |
b2brouter_account_id | immutable snapshot (not a live FK) | the account that carried this exchange, captured at call time (per §8). |
b2brouter_invoice_id | provider document id | B2Brouter’s id for the invoice object on this exchange. |
b2brouter_document_ids | provider ids (CDAR / attachments) | other provider-side ids needed to fetch documents/receipts. |
legal_status | AFNOR code (200–213) | authoritative legal lifecycle status, reconstructed from raw status + CDAR. |
raw_status_payload / raw_payload_hash | raw PA payload + hash | the raw B2Brouter API status + CDAR; the hash makes tampering detectable and supports the ComplianceEventLog (§12). |
idempotency_key | string | dedupe key (B2Brouter invoice id for inbound webhooks; submission key for outbound). |
request_id / retry_attempt | string / int | provider 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
| Field | Type | Meaning |
|---|---|---|
invoice_id | reference | the 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_id | FK → core_banking BankTransaction | the real bank movement this entry allocates. |
allocated_amount | money | the portion of the transaction allocated to this document (supports a transaction split across several documents, and a document collected across several transactions). |
vat_breakdown | list 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_date | date | the value/collection date of the bank movement; the date the VAT becomes due on collection. |
currency | ISO 4217 | currency of the collection (reported when not EUR). |
source | actor/system | who/what produced the entry (payment-link settlement, manual allocation by a member, automated matcher, reversal job). |
correction_reversal_link | nullable reference → ledger entry | for a correction or reversal, points at the entry it amends/reverses (append-only correction semantics — see §11.2). |
cumulative_collected_amount | money | running 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[]) | Type | Meaning |
|---|---|---|
submitted_to_b2brouter_at | nullable timestamp | when 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 timestamp | when DGFiP registered the payment-data report for this group (NULL until registered). Set when the group transitions to reported (see the binding below). |
ledger_id | nullable back-reference → tax-report ledger | the 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:
| Group | vat_rate | vat_chargeability | taxable_base | vat_amount | reportability_state |
|---|---|---|---|---|---|
| Goods | 20 % | débits | €1,000 | €200 | not_reportable |
| Services | 20 % | encaissement | €500 | €100 | reportable |
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:
- Unmatched → rematched. If a collection was allocated to the wrong document, append a reversing entry against the wrong document (negative
allocated_amount,reportability_state = superseded) and a fresh entry against the correct one. Both carry the samecorrection_reversal_linkgroup. - Reversal. A returned/charged-back transaction appends a negative entry reducing
cumulative_collected_amount. - Post-report change. If a
reportedentry must change, it is never edited; a correcting entry is appended and re-reported over Flux 10 as a correction, and a ComplianceEventLog (§12) record captures the before/after. Permanent legal-artifact retention means the original reported figure is preserved.
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
| Field | Type | Meaning |
|---|---|---|
event_id | PK (uuid) | unique id of this log record. |
schema_version | int/semver | version of the event payload schema, so replay survives schema evolution. |
actor | actor ref | the member, system job, or external system that caused the transition. |
causation_id | event ref | the event/command that directly caused this one. |
correlation_id | id | groups every record in one end-to-end flow (e.g. one invoice’s whole lifecycle). |
source_system | enum | invoicing | bills | plateforme_agreee | b2brouter | ppf | business_api. |
event_type | enum | the transition (InvoiceFinalized, TransmissionStatusChanged, PaymentCollected, MarkAsAccepted, AuditLogCorrection, conflict_detected, an admin action, …). |
raw_payload_hash | hash | hash 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_id | string | the B2Brouter request id this transition corresponds to (for replay/reconciliation). |
retry_attempt | int | retry counter for the dispatch. |
occurred_at | timestamp | when the transition happened. |
deprecated_by_event_id | nullable event ref → ComplianceEventLog | when 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:
- B2Brouter / PPF — legal transmission status (what was sent/observed on the wire and at the concentrator).
invoicing— the AR commercial lifecycle (issue → settle).bills— the AP processing/payment lifecycle (receive → approve → pay).plateforme_agreee— the provider integration state (the PA adapter’s view; reconstructs the authoritative AFNOR legal status from raw B2Brouter status + CDAR).business_api— the presentation projection (the coarse user-facing label; never a source of truth).
The matrix below classifies the status-bearing facts by how Green-Got handles each:
| Status / fact | Persisted-only | Observed-from-B2Brouter | Emitted-to-PPF/DGFiP | Unsupported / gated | UI-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
- 1. Reform Overview — scope, Green-Got’s role, shared terminology.
- 2. Platform Architecture — 5-corner model, Flux 1 / Flux 10, routing ownership.
- 4. Formats and Invoice Data — the canonical structured-invoice handoff type (not redefined here).
- 5. Lifecycle Statuses — AFNOR XP Z12-012 statuses and the 3-layer mapping.
- 7. E-Reporting — Flux 10 payment-data and cross-border reporting.
- 8. Archiving — storage model: inbound originals stored in core S3 (
bills/); issued invoices stored as the exact transmitted legal artefact (fetched viadownload_legal_url, content-hashed), with regeneration as a display convenience only. - 9. Mandate and Onboarding — the enrollment link and
b2brouter_account_id. - B2Brouter — Sending invoices — outbound JSON contract.
- B2Brouter — Receiving invoices — inbound list/get, webhooks.
- B2Brouter — Payment — payment fields are metadata only.
10. Sources
- B2Brouter receiving invoices and webhooks (inbound, HMAC signature): https://developer.b2brouter.net/docs/receive_integrate_and_manage_received_invoices and https://developer.b2brouter.net/docs/webhooks_guide
- B2Brouter sending invoices end-to-end (outbound): https://developer.b2brouter.net/docs/send_invoices_end_to_end
- B2Brouter mark_as / lifecycle states: https://developer.b2brouter.net/reference/mark-as-invoice
- AFNOR XP Z12-012 lifecycle statuses: https://tradeshift.com/resources/compliance/france-e-invoicing-standards-xp-z12-012-xp-z12-014/
- DGFiP-enabled account → automatic Flux 1 (B2B) and Flux 10 (e-reporting): https://developer.b2brouter.net/docs/dgfip
- E-reporting payment data (VAT on collection): https://www.cleartax.com/fr/en/e-reporting-france
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:
- CTC (Continuous Transaction Controls): the umbrella for real-time/near-real-time tax controls on invoices. Two broad families: clearance and post-audit (below).
- Clearance model: an invoice is not legally issued until a central tax platform validates it (e.g. Italy SDI, Poland KSeF). Synchronous validation; the platform holds the legal copy.
- Post-audit / decentralized model: the invoice is issued and exchanged between the parties (often over a network), with data reported to the authority; no pre-issuance clearance (e.g. France PPF/PA 5-corner, Germany peer-to-peer).
- ViDA (VAT in the Digital Age): the EU package (adopted 2025-03-11) harmonizing digital reporting; from 2030-07 cross-border B2B/B2G invoices must be structured per EN 16931, with broader alignment by 2035.
- EN 16931: the European semantic invoice model (~176 business terms, BT/BG) — syntax-agnostic, serializable to UBL or CII. The common core every EU e-invoice must comply with.
- Peppol: the EU’s de-facto exchange network (BIS Billing 3.0 = a CIUS of EN 16931; UBL syntax; AS4 transport; SML/SMP directory). Country-agnostic at the transport layer.
- CIUS (Core Invoice Usage Specification): a country/network compliant subset of EN 16931 (e.g. Peppol BIS, FatturaPA, France’s profile). Must not break EN 16931 core rules.
- Country pack: the Green-Got module that adds one country’s specifics (format, transport/portal, identifiers, lifecycle, archiving) on top of the shared core.
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 mappers | one transport adapter per PA / per country CTC |
| AFNOR XP Z12-012 status mapper | one lifecycle mapper per country |
| Annuaire routing | one 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):
- the EN 16931 semantic model (the canonical superset) + calculation/validation base rules;
- UBL 2.1 and CII parse/serialize (both are EN 16931 syntaxes; required for FR + DE + the EU);
- a transport adapter trait (send / receive / fetch-status), with Peppol (AS4 + SML/SMP) as the default shared transport;
- a lifecycle abstraction (the canonical internal status + the generic event/transition model of §5/§6);
- an identifier/directory resolver trait;
- the canonical internal error model (§1.1);
- the archiving trait (PAF + durable storage — 8. Archiving).
Country pack (per country):
- clearance vs post-audit orchestration (a clearance pack blocks legal issuance until the hub accepts);
- format(s) / CIUS (Factur-X, FatturaPA, XRechnung/ZUGFeRD, Facturae, KSeF UPL …);
- transport/portal connector (FR PA via B2Brouter, IT SDI, PL KSeF, ES FACe/SII/VeriFactu, DE Peppol/peer);
- identifier scheme + directory (FR SIREN/SIRET + Annuaire, IT Codice Destinatario/PEC, PL NIP/REGON, …);
- lifecycle status set (FR AFNOR 14 codes + CDAR; IT/PL implicit hub states; DE none mandated);
- archiving specifics (retention 4–10 yr; the audit-trail/SAE rules).
3.1 The variation surface (representative countries)
| Dimension | France | Italy | Spain | Germany | Poland |
|---|---|---|---|---|---|
| Model | Post-audit, 5-corner (PPF/PA) | Clearance (SDI) | Hybrid (FACe B2G; SII/VeriFactu B2B) | Post-audit, peer-to-peer | Clearance (KSeF) |
| Legal issuance | on exchange | on SDI acceptance | on issue | on issue | on KSeF acceptance |
| Format(s) | Factur-X, UBL, CII | FatturaPA (UBL CIUS) | Facturae (B2G), UBL (B2B), VeriFactu hash | XRechnung, ZUGFeRD | KSeF XML (FA) |
| Transport | PA network (B2Brouter) + Annuaire | SDI hub | FACe / peer + AEAT reporting | Peppol / peer | KSeF hub |
| Identifier | SIREN/SIRET + Annuaire | Codice Destinatario / PEC | NIF + admin codes | USt-IdNr (VAT) | NIP / REGON |
| Lifecycle | AFNOR 14 codes (CDAR) | implicit (SDI receipts) | implicit (FACe/SII) | none mandated | implicit (KSeF) |
| Archiving | 6 yr (10 yr accounting), PAF | 10 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:
- translates the pack’s local/national VAT coding (e.g. Italy’s
NaturaN1–N7, a national exemption register, a CIUS-restricted category subset) into the canonical UNCL5305 category +BT-120/BT-121carriers on ingestion, and back out to the local coding on serialization; - pins the in-force version of the local code list and the VATEX list (AFNOR republishes its code lists ~twice a year; pin per release — see 4. Formats and Invoice Data);
- validates permissible category combinations at the pack edge (the EN 16931
BR-*family — e.g. categoryOcannot coexist with other VAT breakdown groups, reverse-chargeAEis all-or-nothing).
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
plateforme_agreeeis, in effect, the France pack today. Its B2Brouter adapter, Annuaire resolver, AFNOR lifecycle, and Factur-X mapping are exactly the FR pack’s contents. When a second country is added, extract the shared, country-agnostic parts (EN 16931 model, UBL/CII, the traits, Peppol) intoe_invoicing_core+peppol, and rename/scope the France-specific remainder as thefrpack.- The canonical model + mappers in §1.1 are the seam. They were written transport-agnostic precisely so this extraction is mechanical, not a rewrite.
invoicingandbillsstay country-agnostic. They already speak the canonical internal model and never branch on B2Brouter; the same holds for any country.
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
- ViDA — EU Taxation: VAT in the Digital Age ; adoption 2025-03-11
- EN 16931 — The Invoicing Hub — EN 16931 ; CIUS and Extension — European Commission
- Peppol — Peppol BIS Billing 3.0 ; B2Brouter Peppol AP
- France — Fonoa — 5-corner model ; Tradeshift — XP Z12-012/014
- Italy (SDI) — Sovos — Italy e-invoicing
- Spain (SII/VeriFactu/Facturae) — EC — eInvoicing in Spain
- Germany (XRechnung/ZUGFeRD/GoBD) — ecosio — Germany e-invoicing
- Poland (KSeF) — EDICOM — Poland KSeF 2026
- Clearance vs post-audit — Intelligent CIO — post-audit vs clearance
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
| Term | What it is | Crate |
|---|---|---|
| Organisation | The 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 |
| Client | A customer of the organisation — the party the organisation issues an invoice to (AR). | invoicing |
| Supplier | A 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
- B2Brouter is the external Plateforme Agréée (PA) — not a row in our database. “PA” is the role B2Brouter plays; there is no separate “PA” entity.
- We hold one
B2BrouterAccountper Organisation (one per SIREN — see 9. Mandate and Onboarding), plus theMandateConsentrecord (the customer’s grant). Both are organisation-level. The organisation / enrollment holds the currentb2brouter_account_id(the account we transmit through now). B2BrouterAccountis a lightweight wrapper, not a provider-metadata cache. It carries only the fields Green-Got needs to route through and reason about the external account —b2brouter_account_id(the external id),status(active/suspended/closed, as last observed on the provider),dgfip_enabled,routing_scope(0225:SIRENat MVP), andcreated_at. It does not mirror B2Brouter’s full account record; KYB facts (legal name, SIREN/SIRET, address) are sourced from theorganisationcrate, never duplicated here.- Enrollment is a first-class entity (one per SIREN), modeling the onboarding state machine
(9. Mandate §5.2): its
statusincludes the happy-path states and the explicit failure states (KybBlocked,AccountFailed,DgfipEnablementFailed), plusretry_attempt/max_retryfor the retry-vs-terminal policy andinbound_designation_lost_atfor the loss-of-designation off-boarding marker. ADesignationLossAlertpersists the resend-until-ack alert when inbound designation is lost (9. Mandate §5.5). - A
PaTransmissionis the bridge between one of our documents and B2Brouter: it records one regulated exchange (itsdirection, B2Brouter’s document ids, and the AFNOR legal status). It is direction-aware: an outbound transmission references an Invoice (AR) via a nullableinvoice_id; an inbound transmission references the Bill (AP) it is the source of via a nullablebill_id. It never FKs a singleinvoice_idfor both directions. It also persists an immutableb2brouter_account_idsnapshot of the account that actually carried the exchange (for audit / replay / provider migration), distinct from the org’s mutable current account. For outbound, it additionally persists the issued-invoice legal-archive fields (legal_artifact_ref,content_hash,provider_invoice_id,retrieval_timestamp,legal_download_path,validation_version,rendering_version,audit_metadata) — the stored compliance copy, consistent with 8. Archiving §5.2. - A
PaymentAllocationledger entry records one real bank-level AR VAT-collection event allocated to an issued invoice (seller-side only; nobill_id), and aComplianceEventLogrecord is the append-only audit/outbox entry for every legal/status/payment/admin transition. Both are defined canonically in 10. Integration Contracts §11–§12. - A
ReportableAcquisitionrecord is the buyer-side (AP) counterpart, owned bybills: it captures one reportable supplier acquisition for CGI art. 290 e-reporting (Flux 10 acquisition data). It is distinct from the ARPaymentAllocationledger — it references abill_id(never aninvoice_id), carries supplier identity and the acquisition’s tax facts, and is the source of theReportableAcquisitionRecordedevent (10. Integration Contracts §6). It is acquisition data, not payment data, and never aPaymentCollected/PaymentAllocationentry.
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 action | Authoritative 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 access | support |
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)
| From | FK | To | Notes |
|---|---|---|---|
B2BrouterAccount | organisation_id | Organisation | one per SIREN; lightweight wrapper (status, dgfip_enabled, routing_scope, created_at), not a provider-metadata cache |
Enrollment | organisation_id (PK) | Organisation | one per SIREN; onboarding state machine + failure states + retry_attempt/max_retry + inbound_designation_lost_at |
DesignationLossAlert | organisation_id | Organisation / enrollment | resend-until-ack alert on inbound-designation loss |
MandateConsent | organisation_id | Organisation | org-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 / Bill | organisation_id | Organisation | every business record is org-scoped |
Invoice | client_id | Client | the buyer |
Invoice | quote_id? | Quote | if converted from a quote |
Avoir (avo_) | corrects_invoice_id | Invoice | a 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). |
Bill | supplier_id | Supplier | the vendor |
Bill | transmission_id? | PaTransmission | inbound transmission; null for email/upload channels |
PaTransmission | invoice_id? | Invoice | nullable; set only when direction = outbound (AR). NULL for inbound. |
PaTransmission | bill_id? | Bill | nullable; set only when direction = inbound (AP). NULL for outbound. |
PaTransmission | organisation_id | Organisation / enrollment | resolves the current B2Brouter account at call time. |
PaTransmission | b2brouter_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. |
PaTransmission | legal_artifact_ref | (core S3 object ref) | the stored transmitted/PA-generated legal artefact — the compliance copy (outbound). Regeneration is display-only. |
PaTransmission | content_hash | (field) | hash of the stored legal artefact, recorded at retrieval_timestamp; proves the copy is intact and matches the provider record. |
PaTransmission | provider_invoice_id | (field) | B2Brouter’s provider invoice id for the legal artefact. |
PaTransmission | retrieval_timestamp | (field) | when the legal artefact was fetched and content-hashed. |
PaTransmission | legal_download_path | (field) | B2Brouter download_legal_url / /as/legal path the artefact was fetched from. |
PaTransmission | validation_version / rendering_version | (fields) | the validation ruleset and renderer versions in effect at transmission, for provenance. |
PaTransmission | audit_metadata | (field) | provider/audit metadata (request id, status history, validation output) accompanying the stored artefact. |
PaymentAllocation | invoice_id | Invoice | AR collection (money in) only; seller-side. There is no bill_id — AP acquisition reporting is a separate bills-owned record. |
PaymentAllocation | transaction_id | BankTransaction | the real bank movement this entry allocates. |
PaymentAllocation | correction_reversal_link? | PaymentAllocation (self) | nullable; for a correction/reversal, the ledger entry it amends (append-only). |
ReportableAcquisition | bill_id | Bill | AP buyer-side acquisition e-reporting (CGI art. 290, Flux 10 acquisition data). No invoice_id — this is acquisition data, not AR payment data. |
ReportableAcquisition | organisation_id | Organisation / enrollment | every reportable acquisition is org-scoped. |
InvoiceTxMatch | invoice_id, transaction_id | Invoice, BankTransaction | junction; incoming credit (AR) |
BillPayment | bill_id, transaction_id | Bill, BankTransaction | junction; 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 fields — legal_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
- Issue & get paid (AR): Organisation →
Invoice(to aClient, withInvoiceLines) →PaTransmission(outbound; resolves the org’s current account, snapshots the carryingb2brouter_account_id) → legal status flows back → aPaymentLinklets the client pay → the incomingBankTransactionis linked byInvoiceTxMatch(full-amount settlement →Settled). Real bank-level VAT collection is recorded asPaymentAllocationentries (which feed Flux 10 payment data when VAT is on collection), distinct from the AFNOR legal status. - Receive & pay (AP): Supplier → (B2Brouter) →
PaTransmission(inbound) →Billcreated (to aSupplier, withBillLines) → approved & paid on Green Got rails → the outgoingBankTransactionis linked byBillPayment→ billPaid. The bill payment state is abills-internal AP state, separate from the received supplier-invoice’s PA/AFNOR state: Green-Got pushes onlymark_as accepted/refusedto the supplier. There is nopaidPA status pushed for French received invoices —mark_as paid/ CDAR 212 on the received French flow is gated (5. Lifecycle Statuses §14). Where the acquisition is reportable (CGI art. 290),billsrecords aReportableAcquisition(keyed onbill_id) and emitsReportableAcquisitionRecorded, feeding the Flux 10 acquisition-data overlay — distinct from the ARPaymentAllocationledger.
6. Related Documents
- 10. Integration Contracts — the events that move between these entities.
- 9. Mandate and Onboarding — the org-level
B2BrouterAccount+MandateConsent. - invoicing/3. Invoice Domain · bills/2. Received Invoice Domain — the per-crate entity detail.
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.
- GDPR / RGPD: the EU General Data Protection Regulation, the legal framework for processing personal data.
- CNIL: the French data-protection authority. Its retention guidance constitutes a presumption of compliance; departing from it requires documented justification.
- PII (personal data): any information relating to an identified or identifiable natural person — here, the names, contact details, and identifiers of counterparty contacts and quote signatories.
- DSAR (data-subject access request): a request by an individual to exercise their GDPR rights (access, rectification, erasure, portability, restriction).
- Right to erasure (“right to be forgotten”): a data subject’s right to have their personal data deleted — subject to the legal-obligation exception (§5) where the law compels retention.
- Active base / intermediate archive / definitive archive: CNIL’s three data-lifecycle phases (§3). Data moves from active operational use, to restricted-access intermediate archive (legal/dispute value), to long-term definitive archive.
- Data minimisation: collecting and retaining only the personal data necessary for a defined purpose, for no longer than that purpose requires.
- Log masking / redaction: removing or obscuring sensitive values (PANs, tokens, secrets, personal identifiers) from logs and stored payloads so operational telemetry carries no unnecessary PII.
- DPA (data processing agreement): the contract that binds a subprocessor (B2Brouter) to GDPR obligations on Green-Got’s behalf.
- Subprocessor: a third party (here B2Brouter, the PA) that processes personal data on Green-Got’s instructions.
2. Data Categories
Green-Got’s invoicing and archiving stack handles distinct categories of data, each with its own lawful basis and retention rule.
| Category | Examples | Contains PII? | Retention driver |
|---|---|---|---|
| Legal invoice content | Issued-invoice legal artefacts, incoming bill originals, signed quotes, immutable structured invoice data, content hashes | Yes (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 / signatories | Names, emails, phone numbers, signatory identity and signing audit beyond what the invoice itself requires | Yes | Minimised — operational duration (§4) |
| Operational logs | Application logs, request traces, error logs, lifecycle-event telemetry | Sometimes (incidental) | Legitimate interest — short, CNIL-aligned (§4) |
| Webhook payloads | Raw B2Brouter / PA callback bodies, transmission and status callbacks | Sometimes | Minimised — keep audit-relevant fields, drop the rest (§4, §6) |
| Audit events | PAF status history, transaction match, retrieval timestamps, provider/audit metadata | Indirectly (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.
| Phase | Meaning | What lives here at Green-Got |
|---|---|---|
| Active base | Data in active use to accomplish the current purpose; full operational access | In-flight invoices and quotes, recent logs, live contact records |
| Intermediate archive | No longer in active use but retains legal / dispute value; access restricted to authorised personnel | The 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 archive | Long-term preservation for intrinsic value | Beyond 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.
| Element | Contract |
|---|---|
| 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 isolation | Every 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-service | A 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 limitations | Support 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 links | An 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 workflow | Elevated, 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 limits | Bulk/repeated downloads are rate- and volume-limited per role to bound exfiltration risk; export beyond the limit requires elevated authorisation. |
| Immutable access logs | Every 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 review | Standing 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.
| Data | Indicative operational retention | Basis | CNIL référentiel / source |
|---|---|---|---|
| Application / request logs (connection/access logs) | ~6–12 months, then deleted | Legitimate 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 processing | Minimisation | CNIL data-minimisation principle (durées de conservation); category (3) short-TTL vault (§4.1) |
| PII of contacts / signatories not part of invoice content | Active relationship + dispute window (CNIL: 3 years from end of relationship / last contact for prospects) | Contract / legitimate interest | CNIL 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 lifetime | Statutory legal obligation (a)/(b), then product promise (c) — own purpose + lawful basis + controls | CNIL 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) | Permanent | Legitimate 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.
| # | Category | What it covers | Storage form | Lifetime |
|---|---|---|---|---|
| 1 | Permanent canonical audit fields | The legal-artefact contract fields (8. Archiving §5.2): content_hash, provider_invoice_id, retrieval_timestamp, tax-report id, CDAR / status history, validation output, request id | Stored verbatim, structured | Permanent (legal obligation) |
| 2 | Hash-only raw payload evidence | Raw 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 body | Hash kept with the PAF (permanent); body not retained |
| 3 | Encrypted short-TTL raw vault | The full raw body, where retained at all for provider support / dispute investigation | Encrypted vault, access-controlled | Short TTL — auto-expires; never permanent |
| 4 | Never-persist | Secrets, signed download URLs, signatures, API keys/tokens | Never written to durable storage in any form | Never persisted |
| 5 | Redaction before persistence | Fields that must be masked before they are stored: PANs, IBAN metadata, emails, refusal-reason free text | Masked/truncated at the point of capture | Masked 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:
- TTL-start = ingestion time, not first access. The category (3) clock starts when the raw body is ingested into the vault, and runs continuously regardless of whether it is ever read. A “TTL from first access / last access” model is rejected: it would let an untouched raw body live indefinitely and a frequently-touched one renew forever, defeating minimisation. The body auto-expires
ingested_at + TTL; an in-flight support/dispute investigation that still needs the body must extract the needed evidence into the PAF (category (1)) before expiry, not extend the raw body’s life. - Category (3) RBAC — narrower than the archive. The raw-body vault is not customer-facing and is not part of the customer self-service archive. Read access is restricted to a dedicated support/dispute-investigation scope (never ambient, never bulk), every read is written to the immutable access log (§3.1), and standing operational tooling has no read access. Category (3) is access-controlled more tightly than the legal-artefact archive, because it may carry un-minimised personal data the legal artefact does not require.
- Storage shape — a distinct table/store, not a column on the legal record. The encrypted raw body lives in its own short-TTL store (its own table / vault namespace), separate from the permanent legal-artefact record. The legal record carries only category (1)/(2) fields (
raw_payload_hash, ids, codes); it must not gain araw_bodycolumn whose lifetime would then be coupled to the permanent record. Auto-expiry deletes the category (3) row without touching the legal record. This table-vs-field split is gated with the TTL decision below (uncertainties.md W-3).
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:
raw_status_payload/raw_payload_hash(10. Integration Contracts) — the hash is category (2) and travels with the PAF; the raw body is category (3) (encrypted short-TTL vault) where retained at all, otherwise dropped after the audit fields are extracted.- The B2Brouter
422validation error body (b2brouter / 7. API Mechanics) — the extracted validation outcome (codes, request id) is category (1) audit metadata; the raw error body may carry personal data and signed URLs, so it is category (2)/(3) (hash-only, or short-TTL encrypted vault) and any signed URL/secret inside it is category (4) (never persisted).
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).
- Webhook payloads: extract the audit-relevant fields into the PAF, then drop or mask the rest of the raw body rather than retaining it indefinitely.
- Identifiers: prefer opaque internal references over raw PII in logs; where a personal identifier must appear, mask or truncate it.
- Secrets: tokens, signing keys, and provider credentials must never be logged in clear.
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).
| Field | Value |
|---|---|
| Gate status | OPEN — 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 item | What the DPA must bind B2Brouter to |
|---|---|---|
| ☐ | Processing on documented instructions | B2Brouter processes personal data only on Green-Got’s documented instructions (Art. 28(3)(a)). |
| ☐ | Security measures | Appropriate technical and organisational security measures (Art. 28(3)(c), 32). |
| ☐ | DSAR assistance | Assistance in responding to data-subject access / rectification / erasure / portability requests (§5; Art. 28(3)(e)). |
| ☐ | Breach notification | Timely notification to Green-Got of personal-data breaches (Art. 28(3)(f), 33). |
| ☐ | Onward-subprocessor limits | No onward subprocessor without authorisation, flow-down of equivalent obligations (Art. 28(2), 28(4)). |
| ☐ | Deletion / return at end of service | Deletion or return of personal data at end of service, subject to Green-Got’s statutory-retention needs (Art. 28(3)(g)). |
| ☐ | Audit rights | Green-Got’s right to audit / receive evidence of compliance (Art. 28(3)(h)). |
| ☐ | Data location | Documented 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
- 8. Archiving and Audit Trail — the four retention bases (statutory 6 yr VAT / 10 yr accounting, the product archive promise, and permanent non-PII evidence), the PAF, and the B2Brouter contractual-archive terms this appendix complements.
- 3. Actors and Legal Posture — the controller / processor split and residual liability.
- 5. Lifecycle Statuses — the status history that becomes audit-event data.
- 10. Integration Contracts — webhook and inbound payloads that carry the data minimised here.
9. Sources
- CNIL — durées de conservation des données (active base / intermediate archive / definitive archive, minimisation, legal vs operational retention, erasure exceptions): https://www.cnil.fr/fr/passer-laction/les-durees-de-conservation-des-donnees
- CNIL — référentiel gestion des activités commerciales (délibération n° 2021-131 du 23 septembre 2021): billing data retained 10 years (Code de commerce) even after the person is no longer a customer; clients/prospects retained for the commercial relationship then 3 years from end of relationship / last contact. Page: https://www.cnil.fr/fr/la-gestion-commerciale ; PDF: https://www.cnil.fr/sites/cnil/files/atoms/files/referentiel_traitements-donnees-caractere-personnel_gestion-activites-commerciales.pdf ; Légifrance: https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045538574
- CNIL — recommandation relative aux mesures de journalisation: access/connection logs retained on a sliding 6 months–1 year window (extendable only for legal obligation / litigation / post-incident analysis): https://www.cnil.fr/fr/la-cnil-publie-une-recommandation-relative-aux-mesures-de-journalisation
- GDPR — Art. 5 (minimisation, storage limitation), Art. 17 (right to erasure and its legal-obligation exception), Art. 28 (processor / subprocessor obligations).
- BOFiP — mandatory invoice retention (BOI-CF-COM-10-10-30), cross-referenced from 8. Archiving §9.
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, structuralCargo.toml, phasedplan.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 (invoicingAR /billsAP /plateforme_agreeetransport+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/).
| Crate | Role | Compile-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.
| Step | Owner | Produces / persists | Canonical model |
|---|---|---|---|
| 1. Mandate capture | plateforme_agreee enrollment use case | MandateConsent (org-level, status: active) + MandateEvidence (exact legal wording rendered, timestamp, acting member, IP/device) | 9. Mandate and Onboarding |
| 2. B2Brouter account creation | plateforme_agreee → B2Brouter adapter | b2brouter_account_id on the enrollment | b2brouter/3. Onboarding Accounts |
| 3. DGFiP tax-report settings | plateforme_agreee → B2Brouter adapter | tax_report_settings enabled (Flux 1 B2B + Flux 10 e-reporting) | b2brouter/3. Onboarding Accounts |
| 4. Annuaire registration | B2Brouter (on Green-Got’s behalf) | SIREN routed-to-B2Brouter in the Annuaire; B2Brouter becomes the designated PA | 6. Annuaire and Routing |
| 5. Enrollment finalize | plateforme_agreee | enrollment Active; ComplianceEventLog record | 10. 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:
- Stall threshold + terminal
Failedstate. Any enrollment step that calls an external system (B2Brouter account create,tax_report_settings, Annuaire registration) can stall. TheEnrollmentWorkflowcarries a per-step stall threshold (a step non-terminal beyond the threshold escalates) and a terminalFailedstate for a step that hard-fails and cannot be auto-resolved (e.g. a non-takenaccount-create rejection, atax_report_settings5xx that persists past the retry budget, a KYB/signatory-authority failure — 9. Mandate §4.1).Failedis distinct from the handledtaken/adopt-existing and wrong-SIREN-reprovision branches (9. Mandate §5.1/§5.3) — those are not failures. AFailedenrollment leaves the org notActive, so transmission stays gated (parked) until the failure is resolved and enrollment is re-driven. - Mandatory enrollment-failure alert (symmetric with the loss-of-designation alert). A terminal
Failed(or a stall past threshold) must raise a mandatory, not-best-effort alert, mirroring the loss-of-designation alert contract in 9. Mandate §5.5: the customer cannot send or receive until enrolled, so a silently-stuck enrollment is a defect. The alert is dispatched within one workflow cycle of detection, states the failed step and what the customer/ops must do, and re-sends until acknowledged; a still-unacknowledged failure past a threshold escalates to ops. Each transition is recorded in the ComplianceEventLog.
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 / Event | Kind | Owner crate | Use case / handler | Notes |
|---|---|---|---|---|
EnrollOrganisation | command | plateforme_agreee | use_cases::enroll (drives the §2 workflow) | Prerequisite phase; idempotent per SIREN. |
FinalizeInvoice | command | invoicing | use_cases::finalize_invoice | Assigns gap-free number, freezes the AR record, emits InvoiceFinalized. |
InvoiceFinalized | event | invoicing | consumed by plateforme_agreee::rules | Begins the outbound contract. |
SubmitTransmission | command | plateforme_agreee | use_cases::submit_outbound | Maps canonical invoice → B2Brouter JSON, creates PaTransmission, calls B2Brouter. |
TransmissionStatusChanged | event | plateforme_agreee | consumed by invoicing::rules | AR invoice updates its projection of the legal status. |
IngestInbound | command | plateforme_agreee | use_cases::ingest_inbound | From poll or webhook hint; persists inbound PaTransmission, downloads original. |
InboundInvoiceReceived | event | plateforme_agreee | consumed by bills::rules | bills creates the AP Bill (Received state; “structured source / no OCR” is extraction metadata, not a Bill.status). |
MarkReceivedAccepted / MarkReceivedRefused | command | bills (decision) → plateforme_agreee (push) | bills::use_cases::decide_received → PA mark_as | The only confirmed received-French mark_as targets. |
TransactionMatched | event | invoicing | consumed by plateforme_agreee::rules | Full-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 PaymentCollected — ledger write + PaymentCollected first, MarkOutboundInvoicePaid after (see ordering below). |
MarkOutboundInvoicePaid | command | plateforme_agreee | use_cases::mark_outbound_paid | Pushes 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). |
PaymentCollected | event | invoicing (ledger writer) | consumed by plateforme_agreee::rules | One 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). |
ReportableAcquisitionRecorded | event | bills (AP) | consumed by plateforme_agreee::rules | One reportable-acquisition record (CGI art. 290); feeds Flux 10 acquisition data. Keyed on bill_id; not payment data, not PaymentCollected. |
ReportPaymentData | command | plateforme_agreee | use_cases::report_flux10 | Overlay push for reportable ledger entries (AR payment data) and reportable acquisitions (AP acquisition data). |
Design rule — collection ordering: ledger write → PaymentCollected → MarkOutboundInvoicePaid.
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.
| Store | Crate | Backs entity | Canonical ref |
|---|---|---|---|
EnrollmentStore | plateforme_agreee | enrollment (b2brouter_account_id, status), MandateConsent, MandateEvidence | 9. Mandate and Onboarding |
TransmissionStore | plateforme_agreee | PaTransmission (direction-aware, immutable account snapshot) | 10. Integration Contracts §8.1 |
ComplianceEventLogStore | plateforme_agreee | the append-only outbox | 10. Integration Contracts §12 |
InvoiceStore / QuoteStore | invoicing | issued invoices, credit notes, lines, numbering sequences | invoicing/3, invoicing/5 |
AllocationLedgerStore | invoicing | the append-only payment-allocation / VAT-collection ledger | 10. Integration Contracts §11 |
BillStore / SupplierStore | bills | received invoices, supplier master, approval state, bill payments | bills/2 |
ReportableAcquisitionStore | bills | the 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.
| Adapter | Crate | Wraps | Mapping responsibility |
|---|---|---|---|
| B2Brouter HTTP client | plateforme_agreee | account 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 mapper | plateforme_agreee (en16931) | EN 16931 BT/BG terms, Factur-X profile | canonical structured invoice → format terms; validated against the EN 16931 / FNFE-MPE spec at generation. |
| Yousign client | invoicing (shared src/yousign/) | quote signature | extracted to a top-level crate before signature ships (readme §Shared yousign crate). |
| OCR client | bills | structured extraction for email/upload channels | PA-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.
| Workflow | Crate | Triggers | Activities (external calls) |
|---|---|---|---|
EnrollmentWorkflow | plateforme_agreee | EnrollOrganisation | account create, tax_report_settings, Annuaire registration, finalize |
OutboundTransmissionWorkflow | plateforme_agreee | InvoiceFinalized (post-enrollment) | map+submit to B2Brouter, durable retry under the rate budget |
InboundPollWorkflow | plateforme_agreee | Temporal schedule — hourly (+ webhook hint as early-poll trigger) | poll received list, GET invoices/{id}, download originals, dedupe on the B2Brouter invoice id |
StatusReconcileWorkflow | plateforme_agreee | schedule + webhook hint | reconstruct AFNOR status from raw + CDAR, emit TransmissionStatusChanged |
DesignationSweepWorkflow | plateforme_agreee | Temporal schedule — at least daily | sweep 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) |
Flux10ReportingWorkflow | plateforme_agreee | PaymentCollected (reportable AR payment data) and ReportableAcquisitionRecorded (AP acquisition data) | overlay push (payment + acquisition); correction re-report |
QuoteSignatureWorkflow | invoicing | quote send | Yousign signature, escalating levels |
ReminderWorkflow | invoicing | per-document config | scheduled reminder cadence |
BillApprovalWorkflow | bills | InboundInvoiceReceived / upload | approval 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:
- Poll cadence: hourly.
InboundPollWorkflowruns on an hourly Temporal schedule (a missed webhook reconciles within ≤ 1h). A webhook hint triggers an immediate out-of-band poll but does not replace the schedule. - Dedupe on the B2Brouter invoice id. A received document is keyed on its B2Brouter invoice id; a poll that re-sees an already-ingested id is a no-op (no duplicate
Bill). This is the same identity the webhook hint and the events-fallback dedupe against (b2brouter/7 §5.2). - Alert cadence binding. The “Received-invoice poll freshness” monitoring alert (b2brouter/7 §10.7.1, “last successful poll < 2× poll interval”) binds poll interval = 1h — so a stalled poller fires after ~2h. The b2brouter/7 “poll interval” referenced there is this hourly cadence.
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:
- Send ≥ 10 test invoices to a staging Green-Got account (across the accepted inbound formats — UBL, CII, Factur-X).
- Verify each arrives within the poll SLA (≤ 1 poll cycle / 1h) and becomes exactly one
bills/Bill. - Verify no duplicates — re-deliver / re-poll and confirm the B2Brouter-invoice-id dedupe produces no second
Bill, and that a webhook hint followed by the scheduled poll does not double-create. - The rehearsal is a launch gate; an untested inbound path does not satisfy the universal-reception obligation (2026-09-01).
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:
- A documented B2Brouter webhook delivery SLA exists (delivery guarantee, ordering, at-least-once semantics) — provider-supplied, not assumed.
- A staging test demonstrates that SLA over a representative window (no lost deliveries, dedupe holds, signature verification stable — b2brouter/6 §4).
- 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:
- Outbound / Flux 10 — legal-deadline exposure (the serious case). Outbound transmission and Flux 10
reporting are absorbed by the durable queue (b2brouter/7 §10.1):
an accepted send is durable and is not lost during an outage. But e-invoicing/e-reporting carry
statutory deadlines, so a queue that holds across a long outage can still breach the law. The
controls:
- Queue TTL against the legal deadline, not wall-clock. Each queued transmission is tracked against its applicable DGFiP deadline; as the time-to-deadline margin crosses the warning threshold it escalates to SEV1 and pages on-call before the breach (b2brouter/7 §10.7.7) — it is never silently retried into the breach.
- Tie to the e-reporting cutoff. The Flux 10 internal cutoff in 7. E-Reporting §4.3 is the deadline the queue TTL/escalation compares against — an outage that threatens that cutoff is a SEV1, not a SEV2.
- On-call ownership. A sustained transient-error rate (B2Brouter unreachable) trips the send-success SLO alert (b2brouter/7 §10.7.1) and pages on-call, who track the outage against deadlines and follow the breach procedure if a deadline is at risk.
- Inbound — visibility latency (the milder case). During an outage the hourly inbound poll simply returns nothing / errors; supplier invoices are not lost (B2Brouter holds them) — Green-Got just sees them late. When B2Brouter recovers, the next poll reconciles and ingests the backlog (dedupe on the B2Brouter invoice id prevents duplicates). The exposure is delayed visibility of received bills, not legal breach, so it is a lower severity than the outbound deadline case — but a poller stalled beyond ~2× the poll interval still alerts (b2brouter/7 §10.7.1).
- No backup PA at MVP. Green-Got does not maintain a second approved platform for failover at MVP: the mitigation for a B2Brouter outage is the durable queue + deadline-aware escalation + on-call recovery above, plus the rehearsed provider-exit package for a permanent provider failure (8. Archiving §5.7). A multi-PA / backup-PA design is explicitly out of scope for MVP and noted as a future option, not a launch dependency.
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 event | Owned by | Action |
|---|---|---|---|
plateforme_agreee | InvoiceFinalized | invoicing | start OutboundTransmissionWorkflow (after enrollment gate) |
plateforme_agreee | PaymentCollected | invoicing | feed Flux 10 payment data for reportable entries |
plateforme_agreee | ReportableAcquisitionRecorded | bills | feed Flux 10 acquisition data (AP, CGI art. 290; keyed on bill_id) |
plateforme_agreee | TransactionMatched | invoicing | trigger MarkOutboundInvoicePaid (AFNOR 212, when legally applicable); no Flux 10 directly; also consumed for reconciliation context |
invoicing | TransmissionStatusChanged | plateforme_agreee | update the AR invoice’s legal-status projection |
bills | InboundInvoiceReceived | plateforme_agreee | create 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 group | Visibility | Crate-backed use cases | Notes |
|---|---|---|---|
invoicing::router() | private (already merged in private_routes()) | issue/finalize invoices, quotes, payment links, reminders | The one e-invoicing router currently wired into business_api. |
bills::router() (intended) | private | list/approve/pay received bills, accept/refuse | To be merged into private_routes() when built. |
plateforme_agreee enrollment routes (intended) | private | start/inspect enrollment, mandate consent capture | Customer-facing enrollment lives behind auth. |
| B2Brouter webhook endpoint (intended) | public (HMAC-verified, not session) | inbound notification hint | Verified 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) | Gates | Status |
|---|---|---|
business_api | the Business API HTTP service (hosts invoicing::router() today) | exists |
plateforme-agreee | the PA Service (enrollment + transmission workers, webhook route) | to add when built |
bills | the AP Service (approval workers, AP routers) | to add when built |
invoicing (worker side) | invoicing Temporal workers (signature, reminders) beyond the API router | to 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:
- API (router) registration — hosted by
business_api. The crate’srouter()is merged intobusiness_api’sprivate_routes()(orpublic_routes()for the HMAC webhook) and is therefore gated byfeature = "business_api". This is the presentation surface; it ships whenever the Business API ships.invoicing::router()is already wired this way today. - Worker (
Service) registration — inserver/src/lib.rs. The crate’s Temporal workers + eventbusrules/are exposed as its ownService, registered in the server’smain()sequence behind the crate’s own feature gate (plateforme-agreee,bills,invoicingworker-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
- Enrollment phase first (§2) — without it no
transmission can run; it owns
MandateConsent/MandateEvidenceand theb2brouter_account_id. - Outbound contract —
invoicingfinalize →plateforme_agreeesubmit → status projection back. - Inbound contract — poll/webhook ingest →
InboundInvoiceReceived→billsBill. - Flux 10 overlay — AR allocation-ledger writes →
PaymentCollected(payment data) and AP reportable-acquisition writes →ReportableAcquisitionRecorded(acquisition data) → Flux 10. Bank settlement also triggersMarkOutboundInvoicePaid(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
- business_domain/readme.md — crate index, DDD layering, scaffolding status.
- 10. Integration Contracts — canonical eventbus + data contracts, the allocation ledger, the ComplianceEventLog, the PaTransmission schema.
- 12. Data Model — consolidated entity-relationship model.
- 9. Mandate and Onboarding — the enrollment link, MandateConsent/MandateEvidence.
- 4. Formats and Invoice Data — the canonical structured-invoice handoff type.
- 5. Lifecycle Statuses — AFNOR statuses and the 3-layer mapping.
- uncertainties.md — the active, classified register of open items blocking/affecting this wiring.
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:
- PPF (Portail Public de Facturation): post-2024, the central Annuaire plus the data concentrator for DGFiP. Not an invoice exchange node.
- PA (Plateforme Agréée): certified approved platform; sole authorized intermediary for invoice exchange and e-reporting. B2Brouter is a PA.
- OD / SC (Opérateur de Dématérialisation / Solution Compatible): non-certified software layer that must operate through a PA. Green-Got is an SC.
- Annuaire: central PPF directory mapping each SIREN/SIRET to its chosen PA and routing code; the routing source of truth.
- Flux 1: domestic B2B e-invoicing data flow.
- Flux 10: e-reporting data flow (B2C, cross-border, payment data).
- CDAR (Compte-Rendu d’Acceptation/Rejet): PPF acknowledgements/delivery receipts flowing back through the sender’s PA.
- Peppol: international interoperability network; France is a national authority since 2025-07-08.
- Chorus Pro: legacy B2G portal for public-administration invoices.
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:
- The PPF no longer sends or receives invoices. Companies cannot exchange invoices through it directly.
- Every in-scope company must route its invoices through a PA.
- The PPF retains two roles: it hosts the central Annuaire and acts as the data concentrator that aggregates invoice data, lifecycle statuses, and e-reporting for DGFiP.
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)
- Hosts the Annuaire: the directory of all in-scope French assujettis (Annuaire-registered entities), mapping SIREN/SIRET to chosen PA and routing code.
- Acts as the data concentrator: receives invoice data, lifecycle statuses, and e-reporting from approved platforms and forwards them to DGFiP.
- Does not exchange invoices between trading parties.
3.2 PA (Plateforme Agréée)
- The sole authorized intermediary for invoice exchange and e-reporting.
- Generates and converts formats (Factur-X / UBL 2.1 / CII) while preserving integrity.
- Queries the Annuaire, routes invoices to the recipient’s PA, and transmits invoice data and e-reporting to the PPF concentrator.
- Two PAs may exchange directly (P2P) if both are Annuaire-registered.
- B2Brouter is the PA Green-Got integrates with.
3.3 OD / SC (Solution Compatible)
- A non-certified software layer (ERP, accounting, invoicing SaaS).
- Cannot send or receive invoices directly; it operates through a PA.
- Green-Got is the SC; it builds invoices and surfaces statuses but delegates all regulated transport to B2Brouter.
3.4 Annuaire
- Pre-populated from INSEE/SIRENE (~4.5M companies).
- Holds each company’s SIREN, SIRET(s), chosen PA, and optional internal routing code.
- Routing key levels:
0225:SIREN(whole entity),0225:SIREN_SIRET(a specific establishment), or an internal code (a department). - Each company must declare its chosen PA and routing scope before 2026-09-01.
3.5 Roles comparison
| Actor | Exchanges invoices? | Talks to DGFiP? | Certified? | Green-Got relation |
|---|---|---|---|---|
| PPF | No (directory + concentrator only) | Yes (aggregates and forwards) | State-run | Indirect, 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) | No | Green-Got itself |
| Annuaire | No (routing lookup) | n/a | State-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.
- Flux 1 — domestic B2B e-invoicing: the transmission of a structured invoice (Factur-X / UBL / CII) from the issuer’s PA to the recipient’s PA, with invoice data and lifecycle statuses reported to the PPF concentrator. This is the only flow in which a structured invoice document actually moves between two parties.
- Flux 10 — e-reporting: the transmission of B2C transaction data, cross-border B2B transaction data, and payment data to DGFiP via the PPF concentrator. No invoice document is exchanged with a counterparty; only metadata is reported.
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:
- A domestic B2B goods sale on VAT-on-debits produces Flux 1 only.
- A domestic B2B service sale on VAT-on-collection produces Flux 1 (the invoice) and a Flux 10 payment-data submission when collected.
- A B2C or cross-border sale produces Flux 10 transaction data, plus a Flux 10 payment-data submission when collected under the collection regime.
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:
FrenchCompany(FR-established assujetti, has SIREN) → Flux 1 (domestic B2B e-invoicing). Reverse-charge operations to an FR company stay on Flux 1 — reverse charge is a VAT-treatment attribute, not a channel discriminator.ForeignCompany(no FR establishment) → Flux 10 (cross-border B2B e-reporting). Subject to the non-established phased deferral (2026-09-01 large/ETI sellers; 2027-09-01 smaller sellers + VAT-liable buyers — see 1. Reform Overview §3.2/§4).Individual(non-taxable consumer) → Flux 10 (B2C e-reporting).PublicEntity(B2G) → Chorus Pro, out of the Flux 1 / Flux 10 reform channels.- Monaco is treated as domestic (Flux 1), not cross-border.
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
- Green-Got (SC) sits at the sender corner as the customer’s software layer. It builds the invoice and submits it to B2Brouter; it never touches the Annuaire, recipient PA, or PPF directly.
- B2Brouter (PA) is the sender’s PA: it converts formats, performs the Annuaire lookup, routes to the recipient PA, and reports data and statuses to the PPF concentrator. When Green-Got’s customer is the recipient, B2Brouter is also the recipient’s PA.
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
- 1. Reform Overview — scope, calendar, and full glossary.
- 3. Actors and Legal Posture — parties and the mandat.
- 5. Lifecycle Statuses — AFNOR XP Z12-012 statuses surfaced through the PA.
- 6. Annuaire and Routing — routing keys and the lookup algorithm.
- 7. E-Reporting — Flux 10 obligations and cadence.
- B2Brouter integration — the PA’s API surface.
8. Sources
- PPF drops the e-invoicing platform role (2024 revision): https://tradeshift.com/resources/compliance/ppf-drops-e-invoicing-platform-role/
- France e-invoicing architecture (five-corner, routing): https://www.fonoa.com/resources/blog/france-e-invoicing-architecture
- PDP / OD / PPF roles: https://www.cleartax.com/fr/en/pdp-od-ppf-france-e-invoicing
- DGFiP approved platforms page: https://www.impots.gouv.fr/facturation-electronique-et-plateformes-agreees
- B2Brouter France annuaire: https://www.b2brouter.net/global/annuaire-france-invoice/
- Annuaire / PPF routing by SIREN: https://www.infos-pa.com/articles/annuaire-ppf-routage-siren-plateformes-agreees
- B2Brouter DGFiP / Flux integration: https://developer.b2brouter.net/docs/dgfip
- CGI art. 290 A (payment-data overlay across e-invoicing + e-reporting): https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000044045416/2022-07-27
3. Actors and Legal Posture
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:
- assujetti: a taxable person carrying out an economic activity, identified by SIREN/SIRET — the Green-Got customer issuing the invoice. Being an assujetti does not require being VAT-liable (franchise-en-base / micro are assujettis and in scope even without a VAT number).
- PA (Plateforme Agréée): certified approved platform; sole authorized intermediary for invoice exchange and e-reporting. B2Brouter is the PA.
- OD / SC (Solution Compatible): non-certified software layer operating through a PA. Green-Got is the SC.
- DGFiP: the French tax administration; registers approved platforms and receives reported data.
- PPF: the public portal — Annuaire plus data concentrator (post-2024, no invoice exchange).
- mandat: the legal authorization empowering a PA to act on a company’s behalf.
- PAF (Piste d’Audit Fiable): the reliable audit trail linking each invoice to its underlying transaction.
- SAE (Système d’Archivage Électronique): certified electronic archiving system.
- eIDAS / QES: EU trust-services regulation and the qualified electronic signature integrity method.
2. Actor Catalogue
| Actor | Identity in the reform | Primary responsibility | Liability |
|---|---|---|---|
| Green-Got | Solution Compatible (SC) | Build the structured invoice, submit it to the PA, surface lifecycle statuses, run onboarding/mandate capture, generate payment links | Software correctness and data accuracy of what it submits; not a regulated intermediary |
| B2Brouter | Plateforme Agréée (PA), Peppol Access Point | Format conversion, Annuaire lookup and routing, transmission to recipient PA, reporting to PPF/DGFiP, audit trail, integrity preservation | Secure transmission, format compliance, audit trail; regulated as an approved platform |
| The customer | assujetti / issuer | Provide accurate invoice content, hold a valid mandat, declare its PA and routing scope in the Annuaire, retain archives | Legally responsible for invoice content and compliance and for archiving |
| The customer’s client | recipient (buyer) | Receive the invoice through its own PA, set business lifecycle statuses (accept / refuse) | Responsible for its own reception and statuses |
| DGFiP | Tax administration / Peppol authority | Register and audit approved platforms, run conformance testing, receive reported data | Enforcement and audit |
| PPF | Public portal (Annuaire + concentrator) | Hold the directory, aggregate invoice data, statuses, and e-reporting for DGFiP | State-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:
- The company (assujetti) — guarantees that invoice content is accurate, complete, and VAT-compliant; that mandatory mentions are present; and that archives are retained for the legal period. See 8. Archiving.
- The PA (B2Brouter) — guarantees that the document is correctly formatted, routed, transmitted, and reported, and that the reliable audit trail (PAF) is preserved.
- The SC (Green-Got) — sits between the two: it captures the mandat during onboarding and submits the company’s content to the PA, but it is neither the legally responsible party for content nor the regulated transmitter.
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:
- The correctness of the software that builds and submits invoices.
- The accuracy of the data it transmits to B2Brouter as supplied/confirmed by the customer.
- Capturing a valid mandat during onboarding before any submission on the customer’s behalf. See 9. Onboarding.
- Surfacing the legal lifecycle status faithfully to the customer.
What Green-Got is NOT responsible for:
- The legal compliance of invoice content — that remains the customer’s responsibility as the assujetti.
- Regulated transmission, routing, format conversion, and reporting to PPF/DGFiP — that is B2Brouter’s responsibility as the PA.
- Archiving as the legally responsible party — the customer remains responsible even when the PA archives. See 8. Archiving.
- Payment processing or settlement — B2Brouter performs none; Green-Got generates payment links on its own rails, separate from the regulated invoice flow.
5. Switching PA
A company may change its approved platform. Switching has two operational consequences:
- The change must be declared in the Annuaire so routing resolves to the new PA.
- Data must be transferred from the old PA to the new one (including in-flight invoices and archives where applicable), to preserve continuity of the audit trail.
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:
- The issuing mandate (scope = issue): the customer’s grant authorising Green-Got (SC) + B2Brouter (PA) to issue/transmit on their behalf. This is the basis for outbound, and it is revoked only through the explicit mandate revocation/supersession path.
- Being the designated PA for inbound in the Annuaire: the customer’s Annuaire entry pointing at our B2Brouter account. This is the basis for inbound (receiving supplier invoices).
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
| Concern | Customer (assujetti) | Green-Got (SC) | B2Brouter (PA) | DGFiP / PPF |
|---|---|---|---|---|
| Invoice content accuracy & VAT compliance | Owner | Faithful submission of supplied data | — | Audit |
| Mandatory mentions present | Owner | Validation assistance | Format validation | Audit |
| Format compliance (EN 16931) | — | Correct mapping to B2Brouter | Owner (conversion/validation) | Conformance testing |
| Routing & transmission | — | — | Owner | — |
| Reporting to PPF/DGFiP (Flux 1 / Flux 10) | — | — | Owner | Receives |
| Reliable audit trail (PAF) | Stays responsible | — | Owner (operational) | Audit |
| Archiving & retention | Owner (legally) | — | May archive on behalf | Access on request |
| Annuaire registration & routing scope | Owner (declares) | Provisions via PA | Registers in Annuaire | Hosts Annuaire |
| Payment / settlement | Owner | Payment links (own rails) | None | — |
| Mandat validity | Grants it | Captures it | Acts 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):
- The exact contractual form and capture mechanism of the mandat in B2Brouter — B2Brouter’s public docs document no explicit “mandate” object; authorization appears handled at the account level (owner/admin roles). The precise legal wording Green-Got must collect, store, and present to the customer is unconfirmed.
- The precise data-transfer obligations and format when a customer switches PA away from B2Brouter (what archives and in-flight state must move, and B2Brouter’s export support) are unconfirmed.
8. Related Documents
- 1. Reform Overview — scope, calendar, and full glossary.
- 2. Platform Architecture — where each actor sits in the five-corner model.
- 8. Archiving — retention, PAF, SAE, and the customer’s enduring responsibility.
- 9. Onboarding — PA registration and operational mandate capture.
9. Sources
- Mandate, retention, audit trail, switching PA: https://www.fiscal-requirements.com/news/5556
- AFNOR archiving guidance (FD Z42-029): https://www.vatupdate.com/2026/05/29/france-2026-e-invoicing-reform-afnor-guidance-on-electronic-invoice-archiving-fd-z42-029/
- PDP / OD / PPF roles and responsibilities: https://www.cleartax.com/fr/en/pdp-od-ppf-france-e-invoicing
- DGFiP approved platforms page: https://www.impots.gouv.fr/facturation-electronique-et-plateformes-agreees
- B2Brouter accounts and onboarding model: https://developer.b2brouter.net/docs/accounts_guide
- PLF 2026 art. 28 (renumbered art. 123) — losing-PA minimal-service window extended 6→12 months (amendment I-880, 27 Nov 2025; modalities decree-pending): https://www.arteva.fr/blog/actualites/article-28-plf-2026-renumerote-article-123/ ; https://www.legifiscal.fr/actualites-fiscales/4351-facturation-electronique-pourra-t-changer-facilement-plateforme-agreee.html ; https://www.lemondeduchiffre.fr/a-la-une/79307-facturation-electronique-le-projet-de-loi-de-finances-2026-consolide-le-calendrier-et-durcit-le-controle-des-plateformes-agreees.html
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
- EN 16931: the European semantic standard for the core invoice model. It defines a fixed set of business terms (BG-n groups, BT-n terms) that every compliant invoice must express, independent of syntax. All three mandated French formats are EN 16931-compliant.
- Socle minimum d’interopérabilité: the “minimum interoperability base” mandated by the French reform — the exact set of three structured formats every Plateforme Agréée (PA) must support: Factur-X, UBL 2.1, CII.
- Factur-X: a hybrid invoice format. A human-readable PDF/A-3 document with an embedded structured XML file (
factur-x.xml) using CII syntax. The French/German joint specification (ZUGFeRD 2.x in Germany). Maintained by FNFE-MPE in France. - UBL 2.1: Universal Business Language 2.1 (OASIS) — a pure-XML invoice syntax.
- CII: Cross Industry Invoice (UN/CEFACT), a pure-XML invoice syntax. The French reform pins the syntax version to D22B (UN/CEFACT CII D22B is “la version de langage retenue dans le cadre de la réforme”, per AFNOR XP Z12-012 §3.9). D16B is the older Factur-X / CEN baseline (CEN/TS 16931-3-3:2017) and is not the reform syntax version — reform flows are D22B. CII is also the syntax embedded inside Factur-X. (Implementation gate: confirm B2Brouter always emits CII D22B — not D16B — for reform flows on staging.)
- PDF/A-3 (ISO 19005-3): an archival PDF profile that permits arbitrary file attachments embedded in the document. Factur-X uses it to carry the XML payload inside the visual invoice.
- ZUGFeRD: the German equivalent specification of Factur-X; the two are technically aligned (ZUGFeRD 2.x ≈ Factur-X).
- Profile: a Factur-X conformance level (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED) defining how many structured fields the embedded XML carries. Distinct from the two reform profiles below, which are the EN 16931 conformance levels every PA must support.
- Reform profiles (EN16931 / EXTENDED-CTC-FR): the French reform defines two profiles of the EN 16931 model that every PA must support (AFNOR XP Z12-012 §3.9, §4.2). EN16931 is a French CIUS (Core Invoice Usage Specification) of the European norm; EXTENDED-CTC-FR is a French EXTENSION of it — a subset of Factur-X EXTENDED — adding reform-specific business terms (additional actors, multi-order/multi-delivery references, contract type, line-level out-of-scope VAT) and adjusting some cardinalities. Both are first-class: an invoice in the reform perimeter is one of these two profiles, expressed in one of the three syntaxes. The profile is declared in BT-24 (e.g.
urn:cen.eu:en16931:2017for EN16931;urn:cen.eu:en16931:2017#conformant#urn:cpro.gouv.fr:1p0:extended-ctc-frfor EXTENDED-CTC-FR). - Cadre de facturation (BT-23): the “invoicing frame” that codifies whether an invoice covers Biens (goods), Services, or Mixte (mixed goods+services lines) — rule BR-FR-08. This is the structured carrier of the goods-vs-services composition the reform mandates (AFNOR XP Z12-012 §4.4.2).
- BT-21 note subject code (UNCL4451): the structured subject code that classifies a legal/commercial note carried in BT-22, so a mandatory mention exists as structured data rather than free text. UNCL4451 is the UN/EDIFACT “Text subject code qualifier” list; its codes carry their own standard titles, which are not the French legal mention names. The standard titles (verified against UNECE UNTDID 4451 D96A — https://service.unece.org/trade/untdid/d96a/uncl/uncl4451.htm) are: AAB = “Terms of payments” (conditions of payment, generic term), PMT = “Payment information”, PMD = “Payment detail/remittance information”, ABL = “Government information”. The French legal mentions (mention d’escompte, indemnité forfaitaire de 40 € pour frais de recouvrement, pénalités de retard, mentions légales RCS) are mapped onto these generic UNCL4451 subjects via the AFNOR national convention.
BLUandBARare NOT valid UNCL4451 codes (verified absent from the D96A list) and must not be asserted as such; any éco-contribution DEEE / routing-indication mention must be re-derived against the AFNOR annex before use. The exact (mention → code) mapping must be pinned against the in-force AFNOR XP Z12-012 §4.4.3 table (rules BR-FR-05/06/07) AND the live UNCL4451 release — AFNOR may prescribe a national convention or register additional codes by annex; do not treat the titles above as the French legal labels. - SIREN: 9-digit identifier of a French legal entity (the company).
- SIRET: 14-digit identifier of a specific establishment = SIREN (9) + NIC (5).
- NIC: 5-digit establishment number; the suffix that turns a SIREN into a SIRET.
- UNCL5305: UN/EDIFACT code list 5305 — “Duty/tax/fee category code”, i.e. the VAT category code.
- UNCL1001: UN/EDIFACT code list 1001 — “Document name code”, i.e. the invoice/document type code.
- VAT on debits / VAT on encaissement: the two French VAT chargeability regimes. For goods (livraisons de biens), VAT is chargeable on delivery — effectively “débits” — and goods cannot elect collection. For services (prestations de services), VAT is chargeable on encaissement (collection — when payment is received) by default; a service provider may opt into the debits regime (“option pour le paiement de la taxe d’après les débits”), which must then be stated on the invoice. The option direction is one-way: only service providers opt for débits; there is no symmetric “goods elect collection” election. Chargeability is qualified per line (§4.4.7) and the document-level statement is derived from the lines.
- B2Brouter: Green-Got’s certified PA; it generates the mandated XML from the JSON contract and routes the invoice. See B2Brouter — Sending invoices.
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.
| Format | Kind | Syntax | Human-readable | Machine-readable | Typical use case |
|---|---|---|---|---|---|
| Factur-X | Hybrid | PDF/A-3 + embedded CII D22B XML | Yes (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.1 | Pure XML | UBL (OASIS) | No | Yes | ERP/EDI integrations, Peppol exchanges, public-sector flows. |
| CII | Pure XML | CII D22B (UN/CEFACT) | No | Yes | EDI 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):
- UBL 2.1 (ISO/IEC 19845, OASIS) — pure XML.
- UN/CEFACT CII D22B — pure XML; the reform syntax version (not the older D16B).
- Factur-X — PDF/A-3 (ISO 19005-3) with an embedded
factur-x.xmlin CII D22B.
Profiles (the two reform profiles, both EN 16931-compliant):
| Reform profile | What it is | BT-24 identifier | When |
|---|---|---|---|
| EN16931 | French CIUS of EN 16931 (tightens, never extends the core) | urn:cen.eu:en16931:2017 | Default for the reform perimeter; carries the full mandatory mention set with line detail. |
| EXTENDED-CTC-FR | French 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 tolerances | urn:cen.eu:en16931:2017#conformant#urn:cpro.gouv.fr:1p0:extended-ctc-fr | When 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):
| Capability | EN16931 | EXTENDED-CTC-FR |
|---|---|---|
| Full EN 16931 core (header, lines, VAT breakdown, totals, mandatory mentions) | Yes | Yes |
| Cadre de facturation B/S/M (BT-23) + structured legal notes (BT-21/BT-22) | Yes | Yes |
| Multi-order / multi-delivery line references | No | Yes |
| Per-line acompte reprise (reference to prior invoice at line level) | No | Yes |
| Additional actors (agent d’acheteur, payeur, agent de vendeur, adressé à, facturant) | No | Yes |
| Out-of-scope VAT (category O) lines mixed with other-category lines on one invoice | No (BR-O-11 forbids) | Yes (BR-O-11 relaxed) |
| ±0,01 € per-line/charge rounding tolerance in foot-of-invoice sums | No | Yes |
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).
- Which Schematron: validation runs against the EN 16931 Schematron rule set (the CEN/Connecting Europe
EN16931-modelrules) plus the French BR-FR / BR-O reform business rules layered on top, at the version current at submission time. Green-Got does not pin a snapshot of these rules into the domain. - Who validates, and when: (1) Green-Got validates its own canonical model against this document before submission (the primary contract — see §7); (2) the PA (B2Brouter) validates the generated artefact (XSD syntax + EN 16931/BR-FR Schematron) at generation/submission as a second line of defence; (3) the recipient’s PA / the PPF Concentrateur may re-validate on receipt. A field’s mandatory/optional status is governed here (§4), not by whichever Schematron version is live.
- Fixture version policy: test fixtures (golden Factur-X / UBL / CII samples) are versioned against the code-list release date (15 May / 15 November) they were validated against, and against the Factur-X spec version (e.g. 1.07.x). When a new code-list release lands, fixtures are re-validated before the new rules go live, exploiting the one-month notice window; a fixture that no longer validates is a mapper change, never a domain restructuring.
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
- The container is a PDF/A-3 file (ISO 19005-3). The visual rendering is what a person sees and what is printed.
- A single XML attachment named exactly
factur-x.xmlcarries the structured data in CII D22B syntax (the reform version; D16B is the older Factur-X baseline). - The PDF’s XMP metadata declares the embedded filename and the conformance profile so a reader knows which fields to expect without parsing the whole XML.
- The visual document and the XML must be consistent: the structured XML is the authoritative data; the PDF is the human-readable face of the same invoice.
3.2 Profiles
Factur-X defines five profiles of increasing structured-data completeness. Higher profiles carry more EN 16931 business terms in the XML.
| Profile | Structured content | EN 16931-compliant | Notes |
|---|---|---|---|
| MINIMUM | Header + totals only; no line detail | No | Effectively a structured cover sheet; insufficient for the French B2B mandate on its own. |
| BASIC WL | Header + totals, “without lines” | No | Document-level VAT breakdown but no line items. |
| BASIC | Header + totals + line items (reduced) | No | Carries 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 model | Yes | Baseline profile Green-Got targets — sufficient for most SME B2B invoices. |
| EXTENDED | EN 16931 + additional terms | Yes (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 profile | EN 16931-compliant | Valid for domestic B2B reform flows? | Reason |
|---|---|---|---|
| MINIMUM | No | No | Header + 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 WL | No | No | “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.) |
| BASIC | No | No | A 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) | Yes | Yes — default | Lowest profile carrying the full mandatory mention set (§4) with line detail. Maps to the reform EN16931 profile. |
| EXTENDED | Yes (superset) | Yes — when needed | Superset 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:
- Start with target profile = EN16931.
- 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.
- 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.
| Field | EN 16931 term | Mandatory? | Notes |
|---|---|---|---|
| Seller SIREN | BT-30 (seller legal registration id, scheme 0002) | Mandatory | The issuer’s 9-digit legal-entity id. BT-30 is the legal registration identifier and carries the SIREN (scheme 0002). |
| Seller SIRET | BT-29 (seller identifier, scheme 0009) | Conditional | Establishment-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, address | BG-4 | Mandatory | Pre-existing VAT-invoice requirement, retained. |
| Seller VAT number | BT-31 | Conditional | Mandatory 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 SIREN | BT-47 (buyer legal registration id, scheme 0002) | Conditional — domestic FR B2B / Flux 1 only | Mandatory 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 SIRET | BT-47 variant | Conditional | When the buyer designates a specific establishment for receipt/routing. |
| Buyer name, address | BG-7 (buyer group): name BT-44, postal address BG-8 | Mandatory | Pre-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 number | BT-48 | Conditional | Required 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) | Mandatory | Distinct delivery / “lieu de livraison” address must be carried when it differs from the billing address. |
| Goods-vs-services composition — cadre de facturation | BT-23 (B / S / M) | Mandatory | The 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 encaissement | line-level category context → document-level derived statement | Mandatory (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 context | Mandatory | The 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 type | BT-3 (UNCL1001) | Mandatory | Invoice / credit note / prepayment etc. See §6. |
| Invoice number | BT-1 | Mandatory | Unique, sequential, gap-free per issuer numbering scheme. |
| Invoice issue date | BT-2 | Mandatory | |
| Payment terms | BT-20 / BT-9 (due date) | Mandatory | Settlement terms and due date. |
| Payment means | BG-16 (BT-81 means code, BT-84 IBAN…) | Mandatory | How payment is to be made (IBAN, transfer, etc.). Metadata only — B2Brouter and the PA settle nothing. See B2Brouter — Payment. |
| Line item: description | BT-153 | Mandatory | Per line of invoice_lines. |
| Line item: quantity | BT-129 | Mandatory | |
| Line item: unit price (net) | BT-146 | Mandatory | |
| Line item: line net amount | BT-131 | Mandatory | Quantity × unit price, net of VAT. |
| Line item: VAT category + rate | BT-151 / BT-152 | Mandatory | Per line. Category from UNCL5305; rate as a percentage. |
| Document totals: total net | BT-109 (sum of lines net) | Mandatory | |
| Document totals: VAT breakdown per rate | BG-23 (BT-116…BT-119) | Mandatory | Taxable base and VAT amount per category/rate. |
| Document totals: total VAT | BT-110 | Mandatory | |
| Document totals: total gross (grand total) | BT-112 | Mandatory | Net + VAT; amount payable. |
| Currency | BT-5 | Mandatory | ISO 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.
- The UNCL5305 VAT category code is the authoritative axis (§5); the goods-vs-services supply type is descriptive only. Chargeability (débits vs encaissement) is meaningful only for taxable, seller-collected supplies — UNCL5305 categories S and Z — where the seller charges and collects French VAT. For AE (domestic reverse-charge), K (intra-EU supply), G (export), E (exempt), and O (out of scope) the seller collects no VAT, so there is no chargeability mention: those lines carry the exemption/reverse-charge reason (BT-120 text and/or BT-121 VATEX code) instead, and the stored chargeability field is null / N-A for them.
- Per line, Green-Got stores the VAT category code (UNCL5305), the rate, the goods-vs-services supply type (descriptive), and — only for S/Z taxable lines — the chargeability basis (débits vs encaissement). A taxable goods line (S/Z) is on débits; a taxable services line (S/Z) is on encaissement unless the provider opted for débits.
- Document-level values are projections of the lines, never independent inputs:
- the cadre de facturation BT-23 (B / S / M) is derived from whether the lines are all goods, all services, or both;
- the VAT breakdown (BG-23) aggregates the line categories/rates;
- the chargeability mention (“TVA sur les débits” / “TVA sur les encaissements”, incl. the option statement) is derived from the S/Z taxable lines’ chargeability basis only; AE/K/G/E/O lines contribute no chargeability mention.
Mixed goods/services behaviour. When an invoice carries both goods and services lines:
- BT-23 = M (Mixte).
- Each taxable (S/Z) line keeps its own chargeability: taxable goods lines on débits, taxable services lines on encaissement unless the seller opted for débits (in which case the option statement renders once at document level). AE/K/G/E/O lines carry no chargeability.
- If the taxable services lines were issued under the débits option, the single document-level “option pour le paiement de la taxe d’après les débits” mention is emitted; if not, the document carries the encaissement basis for those lines. The derivation is deterministic from the lines — there is no separate document-level toggle to keep in sync.
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 mention | BT-21 code (to pin vs AFNOR annex) | UNCL4451 standard title | Typical content | When |
|---|---|---|---|---|
| Recovery fee — indemnité forfaitaire de recouvrement | PMT | “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 retard | PMD | “Payment detail/remittance information” | Penalty rate / terms applicable on late payment | Mandatory on B2B invoices. |
| Discount terms — escompte | AAB | “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égales | ABL | “Government information” | RCS / N° registre des métiers, capital social, etc. | Pre-existing legal-footer mentions. |
Code-list caveat.
BLU(éco-contribution DEEE) andBAR(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:
- Merge at finalize. The org defaults are resolved into concrete
(BT-21, BT-22)pairs and merged with any per-invoice overrides — a per-invoice note with the same subject code replaces the org default for that subject; additional per-invoice notes are appended. The merged result is written intolegal_notes[]on the (immutable) invoice, so a later change to an org default never mutates an already-issued invoice. - Mandatory-validation failure path. Before finalize, validation checks that every legally-mandatory subject for the operation is present in the merged
legal_notes[](the recovery-fee, late-penalty, and discount notes are mandatory on B2B invoices). A missing mandatory note rejects finalize (it is a domain-level mandatory-mention failure per §4 design rule), surfaced to the issuer — it is never silently dropped or deferred to B2Brouter’s Schematron.
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.
| Code | Meaning | Typical use |
|---|---|---|
| S | Standard rate | Ordinary taxable domestic supply at the standard or a reduced positive rate. |
| Z | Zero rated | Taxable at 0%. |
| E | Exempt | Exempt from VAT (with statutory exemption reference). |
| AE | VAT Reverse Charge | Buyer accounts for VAT (autoliquidation). |
| K | VAT exempt for intra-community supply of goods | Intra-EU supply of goods to a VAT-registered buyer. |
| G | Free export item, tax not charged | Export of goods outside the EU. |
| O | Services outside scope of tax | Operation 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):
| Code | Rate rule | Exemption/reverse-charge reason carrier |
|---|---|---|
| S | rate > 0 (BR-S-05) | none |
| Z | rate = 0 exactly (BR-Z-05) | none |
| E | rate 0 | BT-120 text and/or BT-121 VATEX code (BR-E-10) |
| AE | rate 0 | BT-120 text and/or BT-121 VATEX code — VATEX-EU-AE for reverse charge (BR-AE-10) |
| K | rate 0 | BT-120 text and/or BT-121 VATEX code (BR-IC-10) |
| G | rate 0 | BT-120 text and/or BT-121 VATEX code (BR-G-10) |
| O | no rate / out of scope | BT-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:
- BR-O-11 — category O is exclusive. An invoice with a category O VAT breakdown group shall not contain any other VAT breakdown group (https://docs.peppol.eu/poacc/billing/3.0/rules/ubl-tc434/BR-O-11/). Under the EN16931 profile, O cannot coexist with S/Z/E/AE/K/G; mixing O with other categories on one invoice is permitted only under EXTENDED-CTC-FR (BR-O-11 relaxed — §2.1/§3.3).
- AE is all-or-nothing. Domestic reverse-charge (AE) applies across the whole invoice: AE lines/charges and non-AE taxable lines are not mixed on one invoice (BR-AE family). An AE invoice carries the reverse-charge reason (VATEX-EU-AE) and zero seller VAT.
- K / G (intra-EU supply / export) are likewise reason-carrying, zero-VAT operations and are not freely mixed with standard taxable (S) lines without the appropriate breakdown groups and reasons.
- S and Z standard/zero-rated lines may coexist (both are taxable seller-collected categories with their own rates and breakdown groups).
| First category → / Other category ↓ | S | Z | E | AE | K | G | O |
|---|---|---|---|---|---|---|---|
| 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.
| Code | Meaning (FR) | Description |
|---|---|---|
| 380 | Facture commerciale | Commercial invoice — the default document type. |
| 381 | Avoir / note de crédit | Credit 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. |
| 384 | Facture rectificative | Corrected invoice — replaces/rectifies a previously issued invoice. Enumerated by AFNOR XP Z12-012 §4.1 (380 = Facture Commerciale, 381 = Avoir, 384 = Facture Rectificative). |
| 386 | Facture d’acompte | Prepayment / advance-payment invoice (acompte). |
| 389 | Facture auto-facturée | Self-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:
- Acompte (prepayment) invoice → type 386 (facture d’acompte).
- Progress / staged / partial billing → a normal invoice (380) per stage, each billing the stage’s lines; in EXTENDED-CTC-FR a line may carry a reprise d’acompte reference to the prior acompte invoice (per-line reference to a prior 386), so prior prepayments net out automatically (AFNOR §4.3.2). The “already-paid” total (BT-113) carries cumulative prepayments at document level.
- Correction of a finalized invoice → a 381 (avoir) that reverses it, or a 384 (facture rectificative) that supersedes it — never an edit of the original.
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-3 — BT-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
- B2Brouter — Sending invoices — JSON contract, line attributes,
type_document. - B2Brouter — Formats & validation — generation, XSD + Schematron validation, validate endpoint.
- 6. Annuaire and Routing — how the buyer SIREN routes the invoice.
- B2Brouter — Payment (
b2brouter/4_sending_invoices.md) — payment fields are metadata only. - 5. Lifecycle Statuses §10 + §9 — the status half of the format-transposition mapping (this doc’s §9 is the field half).
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:
- Canonical internal field — the field on Green-Got’s rich internal model (an EN 16931 superset, see §6 Design rule).
- EN 16931 term (BT/BG) — the semantic business term. These BT/BG numbers are the conventional ones; the authoritative number is validated at implementation (see the Invariant below).
- Syntax (Factur-X/UBL/CII) — carried by all three mandated syntaxes via the same EN 16931 term (the three are semantically equivalent, §2); “(via B2Brouter generation)” where B2Brouter builds the XML node from the structured JSON rather than a 1:1 top-level field.
- B2Brouter JSON field — the field name in the JSON contract Green-Got submits (B2Brouter — Sending invoices); “(via B2Brouter generation)” where no documented top-level JSON field exists and B2Brouter derives the value.
- Business-app (display) — how the field surfaces in the
business_apiinvoice presentation. - Mandatory? — governed by §4, not by the B2Brouter API surface.
9.1 Seller
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Seller legal name | BT-27 | Seller name | (via B2Brouter generation — sender = account) | Issuer name | Mandatory | Sender is the customer’s own B2Brouter account; seller block derives from account/KYB, not per-invoice JSON. |
| Seller SIREN | BT-30 (scheme 0002) | Seller legal registration id | (via B2Brouter generation — account cin_value, scheme 0002) | Issuer SIREN | Mandatory | 9-digit legal-entity id; sourced from the account CIN (cin_scheme 0002), not the TIN (which carries the VAT number). |
| Seller SIRET | BT-29 (scheme 0009) | Seller id (SIRET) | (via B2Brouter generation) | Issuer SIRET | Conditional | Establishment-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 number | BT-31 | Seller VAT identifier | (via B2Brouter generation — account tin_value, tin_scheme 9957) | Issuer VAT no. | Conditional | French 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 address | Mandatory | Registered address, sourced from the account/KYB. |
9.2 Buyer
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Buyer legal name | BT-44 | Buyer name | contact_id → contact name | Customer name | Mandatory | Buyer is a B2Brouter contact; contact_id references it. |
| Buyer SIREN | BT-47 (scheme 0002) | Buyer legal registration id | contact_id → contact id (SIREN) | Customer SIREN | Conditional — domestic FR B2B / Flux 1 | The 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 SIRET | BT-47 variant | Buyer id (SIRET) | contact_id → contact establishment | Customer SIRET | Conditional | When the buyer designates a specific establishment for receipt/routing. |
| Buyer VAT number | BT-48 | Buyer VAT identifier | contact_id → contact VAT | Customer VAT no. | Conditional | Required for intra-EU / reverse-charge operations. |
| Buyer billing address | BG-8 (BT-50…BT-55) | Buyer postal address | contact_id → contact address | Customer address | Mandatory | The “adresse de facturation”. |
| Buyer delivery address | BG-15 (BT-75…BT-80) | Deliver-to / “lieu de livraison” | delivery address fields (via B2Brouter generation) | Delivery address | Mandatory when ≠ billing | Distinct delivery address must be carried when it differs from billing (§4). |
| Buyer reference | BT-10 | Buyer reference | buyer_reference | Buyer reference | Conditional (often required B2G) | Cost-centre / Chorus “code service”; mandatory toward public buyers. |
9.3 Invoice header
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Invoice number | BT-1 | Invoice number | number | Invoice no. | Mandatory | Unique, sequential, gap-free. |
| Issue date | BT-2 | Issue date | date | Issue date | Mandatory | |
| Due date | BT-9 | Payment due date | due_date | Due date | Mandatory | Settlement deadline (§4). |
| Service/delivery date | BT-72 (BG-14 period) | Actual delivery date / invoicing period | (via B2Brouter generation) | Service date | Conditional | “Date de livraison/prestation” when it differs from the issue date. |
| Currency | BT-5 | Document currency code | currency | Currency | Mandatory | ISO 4217; EUR for domestic French B2B. |
| Document type code | BT-3 (UNCL1001) | Invoice type code | type_document | Document type | Mandatory | Enumerated 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) | Mandatory | Goods-vs-services composition + VAT nature; expressed through the VAT category code (§5). |
| VAT-on-debits / encaissement option | document-level statement | Payment terms / tax note | payment_terms (AAB) / (via B2Brouter generation) | VAT regime mention | Mandatory (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 reference | BT-13 (purchase order) / BT-14 (sales order) | Order reference | ponumber / order_number | PO / order ref | Conditional | ponumber = buyer PO (BT-13); order_number = seller’s order id (BT-14). |
9.4 Lines (invoice_lines_attributes)
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Line group | BG-25 | Invoice line | invoice_lines_attributes[] | Line row | Mandatory | Each element is one invoice line. |
| Line description | BT-153 | Item name/description | invoice_lines_attributes[].description | Description | Mandatory | |
| Line quantity | BT-129 | Invoiced quantity | invoice_lines_attributes[].quantity | Qty | Mandatory | |
| Line unit price (HT) | BT-146 | Item net price | invoice_lines_attributes[].price | Unit price | Mandatory | Net of VAT. |
| Line net total | BT-131 | Line net amount | invoice_lines_attributes[].* (qty × price, via generation) | Line total HT | Mandatory | |
| Line VAT category code | BT-151 (UNCL5305) | Line tax category | invoice_lines_attributes[].taxes_attributes[].category | (VAT badge) | Mandatory | S/Z/E/AE/K/G/O — see §5. |
| Line VAT rate | BT-152 | Line tax rate | invoice_lines_attributes[].taxes_attributes[].percent | VAT % | Mandatory | Percentage; 0 for AE/K/G/E/O. |
| Line tax name | (tax label) | Tax scheme name | invoice_lines_attributes[].taxes_attributes[].name | (tax label) | Conditional | B2Brouter taxes_attributes.name (e.g. “VAT”/“TVA”). |
9.5 VAT breakdown (per category)
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| VAT breakdown group | BG-23 | VAT breakdown (per category) | (via B2Brouter generation — aggregated from line taxes_attributes) | VAT summary rows | Mandatory | One row per category/rate. |
| Taxable base per category | BT-116 | Category taxable amount | (via B2Brouter generation) | Base HT (per rate) | Mandatory | |
| VAT category code | BT-118 (UNCL5305) | Category code | (via line taxes_attributes[].category) | (per-rate category) | Mandatory | |
| VAT rate per category | BT-119 | Category rate | (via line taxes_attributes[].percent) | Rate | Mandatory | |
| VAT amount per category | BT-117 | Category tax amount | (via B2Brouter generation) | VAT (per rate) | Mandatory | |
| Exemption / reverse-charge reason | BT-120 (text) / BT-121 (VATEX code) | VAT exemption reason text/code | (via B2Brouter generation) | Exemption mention | Per-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 field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Total net (HT) | BT-106 / BT-109 (tax exclusive) | Sum of line net / tax-exclusive amount | (via B2Brouter generation) | Total HT | Mandatory | BT-106 = sum of line net; BT-109 = tax-exclusive total. |
| Total VAT | BT-110 | Total VAT amount | (via B2Brouter generation) | Total VAT | Mandatory | |
| Total gross (TTC) | BT-112 | Tax-inclusive amount | (via B2Brouter generation) | Total TTC | Mandatory | Net + VAT; grand total. |
| Already-paid / prepaid | BT-113 | Paid amount | (via B2Brouter generation) | Prepaid | Conditional | Sums of prepayments already settled. |
| Amount due | BT-115 | Amount due for payment | (via B2Brouter generation) | Amount due | Mandatory | TTC − already-paid. |
9.7 Payment means
| Canonical internal field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Payment means group | BG-16 | Payment instructions | (payment fields below) | Payment block | Mandatory | Metadata only — B2Brouter/the PA settle nothing (§4, 5. Lifecycle Statuses §3.4). |
| Payment method | BT-81 (means type code) | Payment means type code | payment_method | Payment method | Mandatory | |
| Payment method text (PMT) | note PMT (BT-21/BT-22) | Payment means text / recovery-fee note | payment_method_text | Payment method text | Mandatory (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 IBAN | BT-84 | Payment account id (IBAN) | iban | IBAN | Conditional (mandatory for transfer) | |
| Remittance information (PMD) | BT-83 / note PMD (BT-21/BT-22) | Remittance information / late-penalty note | remittance_information | Reference | Mandatory (native) | B2Brouter France IssuedInvoice native field; carries the PMD mention (pénalités de retard). Missing it → HTTP 422. |
| Creditor reference | BT-90 | Creditor reference (SEPA) | creditor_reference | Creditor ref | Conditional | |
| Payment terms (AAB / due date / penalties / discount) | BT-20 / BT-9 | Payment terms | payment_terms | Payment terms | Mandatory (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 field | EN 16931 term (BT/BG) | Syntax (Factur-X/UBL/CII) | B2Brouter JSON field | Business-app (display) | Mandatory? | Notes |
|---|---|---|---|---|---|---|
| Supporting attachment | BG-24 (BT-122 ref, BT-125 binary) | Additional supporting document | (via B2Brouter generation / attachment upload) | Attachment | Conditional | Annexes; for Factur-X the visual PDF/A-3 is the carrier (§3). |
| Preceding invoice reference | BG-3 (BT-25 number, BT-26 issue date) | Preceding invoice reference | (via B2Brouter generation) | Corrected invoice ref | Conditional | A 381 (avoir) or 384 (rectificative) references the preceding invoice(s) via BG-3 / BT-25 / BT-26 — not 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
- FNFE-MPE — Factur-X specification and profiles (only EN 16931 and EXTENDED are EN 16931-compliant; BASIC is not): https://fnfe-mpe.org/factur-x/
- UNECE UNTDID 4451 (D96A) — Text subject code qualifier (UNCL4451): https://service.unece.org/trade/untdid/d96a/uncl/uncl4451.htm
- Peppol BIS Billing 3.0 — UNCL5305 VAT category code list: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/
- Peppol BIS Billing 3.0 — EN 16931 BR-* rules (BR-S-05, BR-Z-05, BR-O-11, BR-AE-10, etc.): https://docs.peppol.eu/poacc/billing/3.0/rules/ubl-tc434/
- Peppol BIS Billing 3.0 — VATEX exemption-reason code list (VATEX-EU-AE for reverse charge): https://docs.peppol.eu/poacc/billing/3.0/codelist/vatex/
- BOFiP — détermination du redevable de la TVA / autoliquidation domestique (CGI art. 283-2 sexies déchets, 283-2 nonies sous-traitance bâtiment): https://bofip.impots.gouv.fr/bofip/3218-PGP.html
- EN 16931-1:2017 semantic model and UNCL code lists (UNCL5305 VAT category, UNCL1001 document type) — referenced via the FNFE-MPE and Peppol specifications above.
- Melasoft — e-invoicing France UBL / CII / Factur-X: https://www.melasoft.com/post/e-invoicing-france-ubl-cii-facturx
- Facturwise — mandatory invoice fields in France: https://www.facturwise.com/en/blog/mandatory-invoice-fields-france-guide
- Service-public (entreprendre) — reform overview and mandatory mentions: https://entreprendre.service-public.gouv.fr/actualites/A15683
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
- PA (Plateforme Agréée): State-approved platform; the sole intermediary for domestic B2B invoice exchange and e-reporting. Green-Got uses B2Brouter as its PA.
- SC (Solution Compatible): software layer (ERP/invoicing SaaS) that cannot exchange invoices directly and works through a PA. Green-Got is an SC on top of B2Brouter.
- PPF (Portail Public de Facturation): post-2024 role is the central Annuaire (routing directory) plus the data concentrator that aggregates invoice lifecycle statuses and e-reporting for DGFiP. The PPF no longer exchanges invoices.
- DGFiP: the French tax authority; ultimate consumer of the mandatory lifecycle statuses and e-reporting data via the PPF concentrator.
- AFNOR XP Z12-012: the French standard (February 2026 version, in force — replaces the November 2025 version) defining the regulated invoice lifecycle statuses — 4 mandatory and a set of recommended statuses. The legal lifecycle.
- CDAR (Compte-Rendu d’Acceptation/Rejet): PPF delivery/acceptance-rejection receipt messages that carry lifecycle status updates back to the sender. B2Brouter surfaces CDAR onto the invoice object.
- Flux 1: B2B domestic invoice flow (the e-invoicing flow), through which lifecycle statuses are exchanged.
- Flux 10: e-reporting flow (B2C / cross-border / payment data). Has its own tax-report lifecycle, distinct from the invoice lifecycle.
- OUTBOUND (issued): invoices Green-Got issues on behalf of a customer (accounts receivable; owned by the invoicing crate).
- INBOUND (received): supplier invoices a customer receives (accounts payable; owned by the bills crate).
- Legal lifecycle: the AFNOR status sequence transmitted to the PPF concentrator. The regulatory record.
- API status: the state value B2Brouter exposes on an invoice object via its REST API and webhooks.
- Internal status: the projected status Green-Got persists and shows to users.
- Source of truth: for a given fact, the single layer that authoritatively defines it.
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:
| Layer | Vocabulary | Owner / source of truth | Purpose |
|---|---|---|---|
| 1. AFNOR XP Z12-012 legal statuses | Déposée, Rejetée, Refusée, Encaissée, … (French) | The regulation; transmitted to and reconciled by the PPF concentrator | The legal lifecycle record. What DGFiP sees. |
| 2. B2Brouter API statuses | new, sending, sent, accepted, registered, … (English) | B2Brouter (the PA), via REST API + webhooks | The PA’s operational state of the transmission. How Green-Got learns what happened. |
| 3. Green-Got internal statuses | Draft, 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:
| Mapper | Maps the internal model to/from… | Purpose |
|---|---|---|
| AFNOR mapper | the AFNOR legal status (codes 200–213) | the regulatory record reported/derived per the standard |
| PA / B2Brouter mapper | the PA’s API status + CDAR payload | what the transport (B2Brouter today, another PA tomorrow) understands |
| Business-app mapper | the 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:
*_from_b2brouter_data(...)— map an inbound B2Brouter API status / CDAR payload onto the canonical internal status (and AFNOR status).*_to_b2brouter_data(...)— map a Green-Got decision (accept / refuse / dispute / mark-paid / …) onto the B2Broutermark_ascall (or send payload).
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–213 — 4 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)
| Code | AFNOR status | English | Meaning |
|---|---|---|---|
| 200 | Déposée | Deposited | Invoice submitted to the sender’s PA and validated; ready for transmission. The entry point of the legal lifecycle. |
| 213 | Rejetée | Rejected (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. |
| 210 | Refusée | Refused (by buyer) | The buyer rejects the invoice on business grounds (wrong amount, disputed goods, etc.). A buyer decision, distinct from a platform rejection. Terminal. |
| 212 | Encaissée | Cashed / payment received | Payment 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)
| Code | AFNOR status | English | Side that emits it | Meaning |
|---|---|---|---|---|
| 201 | Émise par la plateforme | Issued by platform | Issuer PA | The sender’s PA has transmitted the invoice to the recipient’s PA. |
| 202 | Reçue par la plateforme | Received by platform | Recipient PA | The recipient’s PA has received the transmission. Pre-decision. |
| 203 | Mise à disposition | Made available | Recipient PA | The invoice is visible/accessible to the recipient in their tool. |
| 204 | Prise en charge | Taken in charge | Buyer | The buyer acknowledges receipt and begins processing; the payment clock starts. |
| 205 | Approuvée | Approved / Accepted | Buyer | The buyer fully accepts the invoice (good-to-pay). Positive counterpart of Refusée. |
| 206 | Approuvée partiellement | Partially approved | Buyer | Partial acceptance (partial quantity/amount, or conditional). |
| 207 | En litige | In dispute | Buyer or supplier | The invoice is contested without full refusal; suspends the payment period. Non-terminal; resolves to Approuvée, Refusée, or cancellation. |
| 208 | Suspendue | Suspended | Buyer or PA | Processing suspended pending a missing document or clarification. |
| 209 | Complétée | Completed | Buyer | Supplementary elements/documents have been provided to resolve a prior gap (e.g. lift a suspension). |
| 211 | Paiement transmis | Payment transmitted | Payer PA | Payment 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 status | Meaning |
|---|---|
new | Invoice created in B2Brouter, not yet sent. |
sending | Send initiated; transmission to the PPF / recipient PA in progress. |
sent | Transmitted by B2Brouter to the PPF / recipient PA. |
accepted | Recipient (buyer) accepted the invoice. |
registered | Tax-reported / registered (the transmission has been registered with the authority). |
refused | Recipient (buyer) refused the invoice on business grounds. |
paid | The 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. |
closed | Terminal/closed state. |
error | Transmission/processing error (includes platform rejection). |
downloaded | The generated document was downloaded. |
3.2 Received (inbound) API statuses
| B2Brouter status | Meaning |
|---|---|
new | Manually imported received invoice (issued=false). |
received | Arrived via a transport (Peppol, email, B2Brouter network). |
invalid | Validation issue on the received document. |
accepted | Marked accepted by the receiver. |
refused | Marked refused by the receiver. |
paid | Marked 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). |
annotated | Internal 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 status | Meaning |
|---|---|
new | Tax report created, not yet sent. |
sent | Submitted to the authority. |
acknowledged | Receipt acknowledged by the authority. |
registered | Registered by the authority (success terminal). |
refused | Rejected by the authority. |
error | Submission/processing error. |
annulled | Cancelled. |
registered_with_errors | Registered, 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):
| Flow | DGFiP-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:
- Rejetée (213) (platform) and Refusée (210) (buyer) are different rejections at different layers; never conflate them.
- Rejetée does not reach the buyer; the only forward path is correct-and-re-deposit.
- Encaissée (212) is reached from Approuvée (205) / Approuvée partiellement (206), optionally via Paiement transmis (211).
- Suspendue (208) / Complétée (209) form a suspend-resolve loop; En litige (207) resolves to acceptance or refusal.
- Cancellation (Annulée) is exceptional; corrections of finalized invoices use a credit note (avoir, 381) in the invoicing crate.
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 status | B2Brouter API status (issued) | Green-Got internal status (invoicing) | Trigger / source |
|---|---|---|---|
| (none — pre-legal) | (none) | Draft | Green-Got: invoice created/edited, not finalized. |
| (created in PA) | new | Issued | Green-Got: invoice finalized + created in B2Brouter (POST /invoices). |
| Déposée (200) | sending | Submitted | Green-Got: POST /invoices/send_invoice/{id}; B2Brouter begins transmission. |
| Déposée (200) | sent | Submitted | B2Brouter: transmitted to PPF / recipient PA (webhook). |
| Rejetée (213) | error | Rejected | B2Brouter / PPF: platform format/technical rejection (webhook + CDAR). Never reaches buyer. |
| Reçue par la plateforme (202) | sent (+ CDAR) | Delivered | PPF/recipient PA: delivery receipt (CDAR) — delivered to buyer’s platform. |
| Approuvée (205) | accepted | Accepted | Buyer (via recipient PA → CDAR → webhook): business acceptance. |
| Refusée (210) | refused | Refused | Buyer (via recipient PA → CDAR → webhook): business refusal. |
| En litige (207) | accepted / error (+ CDAR) | Disputed | Buyer/PPF: dispute raised (CDAR). |
| Encaissée (212) | observed allegedly_paid (CDAR 212); command mark_as {state:"paid"} | Settled | Green-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.
Token Layer What it is Where it appears paidB2Brouter API — command The generic mark_ascommand 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_datamapper (outbound write)allegedly_paidB2Brouter API — observed The 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_datamapper reads back.the “B2Brouter API status” column of the issued tables (§3.1, §5.1, §10) Encaissée (212) AFNOR legal The legal lifecycle status itself. the canonical internal model + AFNOR mapper Rule: the France ISSUED 212 / Encaissée is OBSERVED as
allegedly_paid, never plainpaid. Plainpaidis only the genericmark_ascommand value. A mapper that expects plainpaidas the observed France issued state, or that fails to reconstruct Encaissée fromallegedly_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:
- Command value (what Green-Got sends). When
MarkOutboundInvoicePaidfires, the PA adapter issues B2Brouter’s genericPOST /invoices/{id}/mark_aswith{ "state": "paid" }.paidis the generic command enum value (confirmed on staging) — it is the input to B2Brouter, not a France lifecycle state. - Observed France state (what Green-Got reads back). On the issued France DGFiP lifecycle, CDAR 212 / Encaissée surfaces on the B2Brouter object as
allegedly_paid(per the B2Brouter France DGFiP guide), not plainpaid. The*_from_b2brouter_datamapper must reconstruct CDAR 212 / Encaissée fromallegedly_paid(and the CDAR code), and must not expect plainpaidas the observed France issued state.
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 status | B2Brouter API status (received) | Green-Got internal status (bills) | Trigger / source |
|---|---|---|---|
| (arrival — pre-decision) | received | Received | B2Brouter: arrived via transport (Peppol/email/network), webhook. |
| (manual ingest) | new | Received | Green-Got: manual upload (issued=false). |
| Rejetée | invalid | Invalid | B2Brouter: validation issue on the received document. |
| Acceptée | accepted | Accepted | Green-Got (receiver): POST /invoices/{id}/mark_as state accepted. |
| Refusée | refused | Refused | Green-Got (receiver): mark_as state refused. |
| Encaissée ⚠️ GATED | paid ⚠️ | 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
- Polling (authoritative): scheduled polling / list-get reconciliation —
GET /accounts/{account}/events?invoice_id={id}(and the per-account event/invoice list) on a Temporal schedule — is the source of record for status arrival. The persisted raw API status + CDAR events reconstructed from polling are what the AFNOR legal status and the internal projection are computed from. - Webhooks (accelerator, non-authoritative): B2Brouter sends an HTTP POST on every state change. The payload carries the new API status. A webhook is a hint that may trigger an earlier poll; it is never the system of record (consistent with §6.2 and §14, inbound is polling-authoritative). Header
X-B2Brouter-Signature: t=<timestamp>,s=<signature>is an HMAC-SHA256 over"{timestamp}.{raw_json_body}"; verify with constant-time compare. Treat duplicate POSTs as possible (idempotency). Tax-report webhooks fire on the terminal state. - CDAR receipts: PPF delivery/acceptance/rejection receipts. B2Brouter applies CDAR outcomes onto the invoice object’s API status; Green-Got persists the raw CDAR events so the AFNOR legal status stays reconstructable.
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).
- Issued invoice (invoicing crate):
Draft(pre-legal),Issued(created in the PA, pre-send), then the legal images —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) — plusCancelled(the rare Annulée error case). - Received invoice (bills crate):
Received(arrival),Invalid(Rejetée 213 on a received doc),Available(203),TakenInCharge(204),Accepted(205),PartiallyApproved(206),Disputed(207),Suspended(208),Completed(209),Refused(210),PaymentTransmitted(211),Paid(Encaissée 212).
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 status | Canonical internal status(es) it projects |
|---|---|
| Draft | Draft |
| Pending | Issued, Submitted, Transmitted, Delivered, Available, TakenInCharge, Accepted, PartiallyApproved, Disputed, Suspended, Completed, PaymentTransmitted |
| Settled | Settled |
| Canceled | Rejected, 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):
| Fact | Source of truth | Notes |
|---|---|---|
| Legal transmission status (what was sent/observed at PPF) | B2Brouter / PPF | reconstructed by plateforme_agreee from raw API status + CDAR; only projected into invoicing. |
| AR commercial lifecycle (issue → settle) | invoicing | the canonical internal commercial state Green-Got owns. |
| AP processing / payment lifecycle (receive → approve → pay) | bills | the canonical internal AP state Green-Got owns. |
| Provider integration state (PA adapter view) | plateforme_agreee | the B2Brouter local/operational state and the authoritative AFNOR mapping. |
| Presentation status (Draft|Pending|Settled|Canceled) | business_api | a projection only; never authoritative. |
| VAT collection / payment data | payment-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.
| Dir | Code | AFNOR legal status | B2Brouter API status | Green-Got internal status | Trigger / source |
|---|---|---|---|---|---|
| OUT | — | (pre-legal) | (none) | Draft | Green-Got: finalizing the invoice. |
| OUT | — | (created in PA) | new | Issued | Green-Got: POST /invoices. |
| OUT | 200 | Déposée | sending → sent | Submitted | Green-Got: POST /invoices/send_invoice/{id}; B2Brouter transmits. |
| OUT | 201 | Émise par la plateforme | sent (+ CDAR) | Transmitted | Issuer PA: handed to recipient PA (CDAR). |
| OUT | 202 | Reçue par la plateforme | sent (+ CDAR) | Delivered | Recipient PA: received the transmission (CDAR). |
| OUT | 203 | Mise à disposition | sent (+ CDAR) | Available | Recipient PA: made available to the buyer (CDAR). |
| OUT | 204 | Prise en charge | accepted (+ CDAR) | TakenInCharge | Buyer: acknowledged, processing started (CDAR). |
| OUT | 205 | Approuvée | accepted | Accepted | Buyer: business acceptance (CDAR → webhook). |
| OUT | 206 | Approuvée partiellement | accepted (+ CDAR) | PartiallyApproved | Buyer: partial acceptance (CDAR). |
| OUT | 207 | En litige | accepted/error (+ CDAR) | Disputed | Buyer: dispute raised (CDAR). |
| OUT | 208 | Suspendue | (+ CDAR) | Suspended | Buyer/PA: processing suspended (CDAR). |
| OUT | 209 | Complétée | (+ CDAR) | Completed | Buyer: supplementary elements provided (CDAR). |
| OUT | 210 | Refusée | refused | Refused | Buyer: business refusal (CDAR → webhook). |
| OUT | 211 | Paiement transmis | observed via CDAR 211 (not plain paid) | PaymentTransmitted | Payer PA: payment initiated/ordered (CDAR). |
| OUT | 212 | Encaissée | observed allegedly_paid (CDAR 212); command mark_as {state:"paid"} | Settled | Green-Got: full payment confirmed (Green-Got rails). Observed France state is allegedly_paid, not plain paid — see §5.3. |
| OUT | 213 | Rejetée | error | Rejected | PPF/platform: format/technical rejection (CDAR + webhook). |
| OUT | — | (Annulée — rare) | closed/error | Cancelled | Green-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) | Received | B2Brouter: arrival via transport / manual upload. |
| IN | 213 | Rejetée | invalid | Invalid | B2Brouter: validation issue on received doc. |
| IN | 203 | Mise à disposition | received | Available | Recipient PA (us): made available in-app. |
| IN | 204 | Prise en charge | mark_as† | TakenInCharge | Green-Got (buyer): acknowledged, processing started. |
| IN | 205 | Approuvée | accepted | Accepted | Green-Got (buyer): mark_as accepted. |
| IN | 206 | Approuvée partiellement | mark_as† | PartiallyApproved | Green-Got (buyer): partial acceptance. |
| IN | 207 | En litige | mark_as† | Disputed | Green-Got (buyer): dispute raised. |
| IN | 208 | Suspendue | mark_as† | Suspended | Green-Got (buyer): suspended pending document. |
| IN | 209 | Complétée | mark_as† | Completed | Green-Got (buyer): document provided. |
| IN | 210 | Refusée | refused | Refused | Green-Got (buyer): mark_as refused. |
| IN | 211 | Paiement transmis | mark_as† | PaymentTransmitted | Green-Got (payer): payment initiated. |
| IN | 212 | Encaissée ⚠️ gated | paid ⚠️ gated | Paid (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):
- Legal lifecycle (AFNOR) — source of truth = PPF concentrator; Green-Got reconstructs it from persisted B2Brouter API status + CDAR.
- Transmission state (API) — source of truth = B2Brouter; ingested via webhook/polling.
- User-facing status (internal) — source of truth = Green-Got; a projection, never the legal record; mandatory AFNOR statuses must always be derivable from what is persisted.
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):
| Concept | What it is | Event | Effect on AFNOR legal status |
|---|---|---|---|
| Settlement (AR) | the payment link is paid in full | TransactionMatched | drives 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 collection | a real bank movement allocated to a document with a VAT breakdown (the payment-allocation ledger — 10 §11) | PaymentCollected | the 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 carrier | explicit 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:
TransactionMatched— the reconciliation fact; drives the AR commercial status →Paidonly (the commercial-axis terminal — the AFNOR-legalSettled/Encaissée is a separate axis). It does not, by itself, set any legal status or emit any tax report.PaymentCollected— the bank-level VAT-collection ledger entry (seller-side AR-only, keyed oninvoice_id); it feeds Flux 10 payment data when VAT is on collection. This is the payment-data source.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 projections — MarkOutboundInvoicePaid 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
- 2. Transmission & Flux 1 — how invoices are routed and transmitted; where the legal lifecycle is exchanged.
- 7. E-reporting (Flux 10) — the separate tax-report lifecycle (B2C / cross-border / payment data).
- b2brouter/5. Receiving Invoices — inbound reception channels and the
mark_asflow. - b2brouter/6. Statuses & Webhooks — B2Brouter status list, webhook signature, polling fallback, CDAR ingestion.
- invoicing/2. Invoice Data Model — the issued (AR) invoice domain model the OUT internal statuses belong to.
- bills/2. Receiving Invoices — the received (AP) invoice domain model the IN internal statuses belong to.
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.
- AFNOR XP Z12-012 (February 2026 version, in force), French e-invoicing lifecycle statuses — boutique.afnor.org — XP Z12-012. Status summary: Tradeshift — France e-invoicing standards XP Z12-012 / XP Z12-014 ; Fonoa — France e-invoicing flow.
- B2Brouter statuses & webhooks — developer.b2brouter.net — get-invoice-states ; mark-as-invoice ; ack-invoice ; webhooks guide.
- B2Brouter inbound — receive, integrate and manage received invoices ; get-invoices.
- B2Brouter DGFiP / Flux — developer.b2brouter.net — dgfip.
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
- Annuaire (national directory): the central directory operated by the PPF. It maps every in-scope French assujetti’s (every Annuaire-registered entity’s) SIREN (and optional finer routing keys) to the PA that entity has chosen to receive its invoices. It is the authority that answers “which platform serves this buyer?”.
- PPF (Portail Public de Facturation): the State portal. Post-2024 it no longer exchanges invoices; it now operates only the central Annuaire and the data concentrator (aggregating lifecycle statuses and e-reporting for DGFiP). See 2. Platform Architecture and 3. Actors and Legal Posture.
- PA (Plateforme Agréée): a State-registered certified platform; the sole intermediary that may exchange invoices and route them. B2Brouter is a certified PA.
- SIREN: 9-digit legal-entity identifier. The primary routing key.
- SIRET: 14-digit establishment identifier = SIREN (9) + NIC (5).
- Routing code / internal code: an optional finer identifier a company can declare in the Annuaire to direct invoices to a specific establishment or internal department.
- 0225: the EAS (Electronic Address Scheme) code identifying the French SIREN/SIRET routing scheme in the Annuaire address syntax.
- INSEE / SIRENE: the national business register from which the Annuaire is pre-populated.
- P2P (same-PA shortcut): when sender and recipient are both served by the same PA, that PA delivers directly without a cross-platform hop.
- B2Brouter Directory lookup: B2Brouter’s own read-only
GET /directory/...endpoint, which resolves a recipient’s B2Brouter account / supported transports / document types — not the national Annuaire. It is a provider convenience lookup, not an authoritative routing source. See §5. - B2Brouter directory cache (non-authoritative): any local cache Green-Got keeps of
GET /directory/...responses. It is a short-TTL, non-authoritative mirror of B2Brouter Directory data — explicitly not the national Annuaire and never a local routing table. It must be named so it cannot be mistaken for the Annuaire (e.g.b2brouter_directory_cache, neverpa_partner_directoryor anything implying it mirrors the Annuaire). See §5. - DGFiP: the French tax authority; the ultimate consumer of e-reporting and lifecycle status data via the PPF concentrator.
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:
- Holds, per registered entity: SIREN, the entity’s SIRET(s), the chosen PA, and any optional routing code.
- Pre-populated with roughly 4.5 million companies, seeded from INSEE / SIRENE.
- Each in-scope company must declare its chosen PA and routing scope in the Annuaire before the reception obligation takes effect (2026-09-01).
- It is a directory only: the Annuaire never carries invoice content. It answers routing lookups and nothing more.
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.
| Level | Identifier form | Scheme | Granularity | When used |
|---|---|---|---|---|
| Legal entity | 0225:SIREN | 0225 | Whole company (all establishments) | Default; one platform receives for the entire legal entity. |
| Establishment | 0225:SIREN_SIRET | 0225 | A specific establishment (SIRET) | When distinct establishments route to different platforms or inboxes. |
| Internal | internal routing code | (declared) | A department / service within the entity | When 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
- The issuer (Green-Got customer) builds the invoice with a mandatory buyer SIREN (optionally SIRET / internal code).
- Green-Got submits the structured invoice to its PA, B2Brouter.
- B2Brouter validates the invoice (format + business rules).
- B2Brouter resolves the recipient: it queries the Annuaire with the buyer’s routing identifier (
0225:SIREN, or the finer key). - The Annuaire returns the recipient PA’s routing address.
- B2Brouter routes the invoice to the recipient PA (or, if the recipient is on B2Brouter too, delivers internally — the same-PA P2P shortcut).
- The recipient PA delivers the invoice to the buyer.
- 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.
| Aspect | National Annuaire (PPF) | B2Brouter directory lookup |
|---|---|---|
| Operated by | PPF (the State) | B2Brouter (the PA) |
| Question answered | “Which PA serves this SIREN?” | “Does this recipient have a B2Brouter account / which transports does it support?” |
| Scope | All ~4.5M French assujettis (Annuaire-registered entities) | B2Brouter’s own network / reachable transports |
| Access from Green-Got | Indirect — B2Brouter queries it during routing; Green-Got never calls it directly | GET /directory/{scheme}/{id} or GET /directory/{country}/{id} |
| Role | Authoritative national routing source | Convenience 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
- 2. Platform Architecture — PPF as directory + concentrator; PA role in the five-corner model.
- 4. Formats and Invoice Data — buyer SIREN as the mandatory routing key.
- 5. Lifecycle Statuses — the statuses that flow back along the routing path.
- 9. Onboarding — how a customer is registered in the Annuaire and how the mandate is captured.
- B2Brouter — Onboarding accounts —
tax_report_settings/dgfipregistration. - B2Brouter — Sending invoices —
GET /directory/...lookup and routing behaviour.
8. Sources
- Fonoa — France e-invoicing architecture (routing, Annuaire, PA roles): https://www.fonoa.com/resources/blog/france-e-invoicing-architecture
- B2Brouter — Annuaire France: https://www.b2brouter.net/global/annuaire-france-invoice/
- Infos-PA — Annuaire PPF, routage SIREN, plateformes agréées: https://www.infos-pa.com/articles/annuaire-ppf-routage-siren-plateformes-agreees
- B2Brouter developer docs — DGFiP registration and directory lookup: https://developer.b2brouter.net/docs/dgfip ; https://developer.b2brouter.net/docs/directory
- B2Brouter France DGFiP e-invoicing & e-reporting guide (
in_dgfip_annuaire: false→ invoice created but no Flux 1 tax report): https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
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:
- For domestic B2B invoiced operations (Flux 1), payment data is NOT carried by a separate Flux 10 submission. It is transmitted by enriching the AFNOR
Encaissée(212) status of the invoice with the collection date and the amount by VAT rate — the mention d’encaissement (MEN, rule BR-FR-CDV-14). That status is the payment-data channel (“ce statut porte les données de paiement”). - For non-invoiced operations (international B2B, B2C), payment data is reported through the separate global Flux 10 payment-data flow.
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.
- E-reporting (transmission de données): the periodic transmission of transaction and payment metadata to DGFiP for transactions not covered by domestic B2B e-invoicing. No structured invoice is exchanged between the parties; only aggregated/structured data is reported.
- Flux 10: the data flow carrying e-reporting metadata to DGFiP through the PPF concentrator. Distinct from Flux 1, which carries domestic B2B invoices.
- Transaction data: the metadata describing a sale — counterparty nature, taxable amounts, VAT rates and amounts, operation category. Reported for B2C and cross-border B2B transactions.
- Payment data (données de paiement): the metadata describing a collection event — collection date, amount collected including VAT, VAT amount per rate, currency. Reported when VAT becomes due on collection (encaissement) rather than on debit.
- VAT on debits (TVA sur les débits): the regime where VAT becomes due when the invoice is issued / the goods are delivered (the “debit”). It is the rule for supplies of goods (delivery/supply, CGI art. 269 — goods cannot elect collection), and the opted option for service providers who elected debits. Payment data e-reporting is not required under this regime.
- VAT on collection / encaissement (TVA sur les encaissements): the regime where VAT becomes due when payment is collected. It is the default for supplies of services, unless the service provider opted for debits. Goods cannot elect collection. Mixed (goods + services) invoices are resolved per line / per chargeability. This regime is the scope condition for payment-data e-reporting (it does not, by itself, trigger an emission — see the canonical chain below).
TransactionMatched: a commercial full-settlement signal — an incoming payment has been matched to an invoice/bill and the document is fully settled. It is a reconciliation fact, not a tax-reporting trigger.PaymentCollected: a bank-level VAT collection/allocation ledger entry — the canonical record of an actual collection (date, amount incl. VAT, VAT breakdown by rate, currency, allocation to aninvoice_id). It is seller-side AR-only (keyed oninvoice_id, never abill_id). It is the internal data source for payment-data e-reporting: for domestic B2B invoiced operations it feeds the enrichedEncaissée/212 MEN (the legal carrier), and for non-invoiced operations (international B2B, B2C) it feeds the separate global Flux 10 payment-data flow. AP acquisition e-reporting is a separate strand carried byReportableAcquisitionRecorded(see §3), not by this AR payment ledger.Encaissée/ MEN (mention d’encaissement): the AFNOR XP Z12-012 legal lifecycle status (code 212) confirming payment received. For domestic B2B invoiced operations it carries the payment data when enriched with the collection date and amount by VAT rate (rule BR-FR-CDV-14) — “ce statut porte les données de paiement”. The AFNOR layer supports partial collection: each partial payment is its ownEncaissée/MEN with the partial amount, and a payment in instalments produces one MEN per collection date. (Green-Got’s product-level payment link is full-amount-only — a Green-Got product choice for the customer link, not a legal restriction on the 212/MEN layer.) See 5. Lifecycle Statuses.- Reverse charge (autoliquidation): a mechanism where the buyer, not the seller, accounts for the VAT. The relevant UNCL5305 VAT category codes are: S (standard rate), Z (zero rate), E (exempt), AE (VAT reverse charge), K (intra-EU supply / VAT exempt under reverse charge), G (export, free of VAT), O (outside scope of VAT). The seller reports no payment data for AE/K/G/E/O operations — CGI art. 290 A excludes operations “pour lesquelles la taxe est due par le preneur” and seller-collected VAT exists only for S (and the zero-amount Z). Only S lines ever produce a payment-data overlay.
- Cross-border B2B: a B2B transaction with a counterparty outside France — intra-EU or extra-EU. In scope for e-reporting, out of scope for e-invoicing. Includes purchases/acquisitions from non-French suppliers, not only sales (see §3).
- AR / AP (accounts receivable / accounts payable): in Green-Got, AR is the invoicing (sales) side and AP is the bills side. E-reporting operation types attach to one side or the other — sales-side operations are owned by AR, acquisition/purchase operations by AP.
- Article 290 / 290 A (CGI): the two articles that define the e-reporting obligation. Art. 290 lists the transaction operations to be reported (sales and certain acquisitions); art. 290 A adds the payment-data obligation for operations where VAT is due on collection. The payment-data obligation explicitly spans operations under both art. 289 bis (domestic e-invoicing) and art. 290 (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:
- 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).
- 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
| Dimension | E-invoicing (Flux 1) | E-reporting (Flux 10) |
|---|---|---|
| What is transmitted | A structured invoice (Factur-X / UBL 2.1 / CII) | Transaction and payment metadata only |
| Recipient | The buyer’s approved platform, then DGFiP via the concentrator | DGFiP via the concentrator (no counterparty delivery) |
| Transactions covered | Domestic B2B between French assujettis | B2C, cross-border B2B (sales and reportable acquisitions), and payment data for non-invoiced operations |
| Overlap | Payment 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 |
| Mechanism | Document exchange + lifecycle statuses (per invoice) | Periodic batched metadata transmission |
| Cadence | Per invoice, on issuance | Statutory 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 document | Yes | No |
| Lifecycle tracking | Yes — AFNOR XP Z12-012 statuses | No 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) | Direction | Owning side | E-invoicing? | E-reporting? |
|---|---|---|---|---|---|
| 1 | B2C sales to non-taxable persons in France | Sale | AR | No | Yes (transaction data) |
| 2 | Intra-EU B2B sales (deliveries/services to EU assujettis) | Sale | AR | No | Yes (transaction data) |
| 3 | Extra-EU B2B sales (exports, services to non-EU assujettis) | Sale | AR | No | Yes (transaction data) |
| 4 | Intra-EU acquisitions of goods (France is destination) | Purchase | AP | No | Yes (acquisition data) |
| 5 | Purchases of goods/services from non-French suppliers where the operation is located in France | Purchase | AP | No | Yes (acquisition data) |
| 6 | Monaco operations (both directions, treated as domestic-equivalent for VAT) | Sale / Purchase | AR / AP | Per nature | Per nature |
| 7 | Payment data (art. 290 A) for sales under the collection regime and for domestic B2B service invoices | Overlay (seller-side collection) | AR only | n/a | Yes when VAT due on collection |
Design rule — AR vs AP e-reporting ownership:
- AR (invoicing) owns sales transaction data AND payment data. Items 1–3 (sales transaction data) and item 7 (payment data) are AR-owned. Payment data is a seller-side collection event: it is emitted by the entity receiving payment, derived from a
PaymentCollectedledger entry on a sale. - AP (bills) owns reportable-acquisition data ONLY. Items 4 and 5 are AP-owned reportable-acquisition records/events, surfaced via the canonical
ReportableAcquisitionRecordedevent (producerbills, consumerplateforme_agreee) — acquisition e-reporting under CGI art. 290. The reportable event is a bill from a foreign supplier in legal scope, not an invoice Green-Got’s customer issues; it is not aPaymentCollectedentry and not payment data. The bills domain surfaces these acquisitions to e-reporting just as invoicing surfaces sales (items 1–3), but AP emits no payment data. A Green-Got customer paying a foreign supplier is a buyer-side disbursement, not a payment-data collection event. - Reverse-charge purchases are still reported as transaction/acquisition data even though their payment data is excluded by art. 290 A.
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 thedaily_transaction_countplaceholder 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.
- Sales side (AR): deliveries and services to foreign assujettis. Excluded from domestic e-invoicing precisely because the foreign counterparty is not reachable through the French annuaire, so their data is reported to DGFiP instead.
- Purchase side (AP): intra-EU acquisitions and purchases of goods/services from non-French suppliers where the operation is located in France are also reportable under art. 290 (items 4–5 above). Green-Got’s customer is the buyer here; the data originates from a supplier bill, not from an invoice the customer issues.
- Monaco / special territories: Monaco is treated as domestic-equivalent for several VAT operations; classify per the operation’s nature rather than assuming “foreign”.
Reportable-acquisition decision algorithm (CGI art. 290, items 4–5). A supplier bill is a reportable acquisition iff all of:
- the supplier is non-French — determined from the supplier’s country/VAT-identification on the bill: no French SIREN/SIRET and a non-
FRcountry (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 - 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
- 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:
- the collection date (the date payment was received);
- the amount collected, including VAT;
- the VAT amount broken down by rate;
- the currency (EUR-only for now in Green-Got).
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:
- Domestic B2B invoiced operations (Flux 1) — the payment data is carried by enriching the invoice’s AFNOR
Encaissée(212) status with the MEN (mention d’encaissement): collection date + amount by VAT rate (rule BR-FR-CDV-14). There is no separate Flux 10 submission for these. The internalPaymentCollectedledger is the data source feeding the enriched 212; the output is the enriched 212/MEN. Because the AFNOR layer supports partial collection, a partialPaymentCollectedproduces its own partialEncaissée/MEN. - Non-invoiced operations (international B2B, B2C) — the payment data is carried by the separate global Flux 10 payment-data flow, derived from the reportable
PaymentCollectedentry.
[gate — MEN enrichment + correction staging/support-gated — L-4 / P-7]The issued-invoicemark_as paidtransition itself is documented — B2Brouter’s API reference listspaidas a valid issued-invoicemark_astarget (mark-as-invoice reference), and the France DGFiP guide maps the observed issued stateallegedly_paidto CDAR 212 / Encaissée (France DGFiP guide). So the paid transition is not “unavailable”: Green-Got sends the generic commandmark_as {state:"paid"}and reads back the observed France stateallegedly_paid(never plainpaid— 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 thepaid/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; themark_as paidtransition and its observedallegedly_paidstate are, by contrast, documented.
Canonical trigger chain (single source of truth):
TransactionMatchedrecords commercial full settlement only — a reconciliation fact, never a tax trigger.PaymentCollectedis the bank-level VAT collection/allocation ledger entry — the canonical record of an actual collection, and the data source for the payment-data obligation.- Payment data is derived from a reportable
PaymentCollected(collection regime, not reverse-charge,reportability_statein scope) and emitted on the channel-appropriate carrier: the enrichedEncaissé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):
acquisition_id— stable identifier of the reportable acquisition (derived from the bill);bill_id— back-reference to the source supplier bill (AP-only — never aninvoice_id);supplier_identity— supplier name + country + VAT identification (the discriminator that made it reportable);operation_category— intra-EU acquisition (item 4) vs France-located purchase from a non-FR supplier (item 5);taxable_base— taxable amount(s) of the operation;vat_breakdown—{ vat_rate, taxable_base, vat_amount }per rate; reverse-charge (self-liquidated) acquisitions carry the self-assessed VAT category (e.g. AE) and are still reported as transaction data;operation_date— the chargeable-event / acquisition date;currency(EUR-only for now in Green-Got);reportability_state—reportable/reported/superseded.
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 regime | Transaction data (242 nonies O) | Payment data (242 nonies P) |
|---|---|---|
| Réel normal — mensuel | At least 3 transmissions / month | At least 1 / month |
| Réel normal — trimestriel | At least 1 transmission / month | At least 1 / month |
| Régime simplifié de déclaration (replaces RSI from 2027-01-01) | At least 1 transmission / month | At least 1 / month |
| Franchise en base (293 B) / remboursement forfaitaire | At least 1 transmission every two months | At 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. 38 — before 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 thedaily_transaction_countplaceholder (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:
- Reconciliation target: every reportable operation and every collection in a period must be emitted to B2Brouter and confirmed accepted into a ledger before an internal cutoff (set comfortably before the legal deadline — e.g. several days ahead of the ~15th).
- Fields the cutoff compares against: the breach check is evaluated against the canonical ledger’s transmission/report timestamps, not the
collection_date(which is the VAT-chargeability date). It compares the internal cutoff tosubmitted_to_b2brouter_at(the event was emitted to B2Brouter) andreported_at/reported_to_dgfip_at(the event was confirmed accepted into a daily ledger / acknowledged by DGFiP). An entry whosesubmitted_to_b2brouter_atis null at the cutoff is “not yet emitted”; one withsubmitted_to_b2brouter_atset butreported_at/reported_to_dgfip_atnull is “not yet acknowledged”. The optionalledger_idback-reference links the entry to the daily ledger that discharges it. (These timestamp fields are defined canonically in10_integration_contracts.md§11.1.) - Count-based legal-minimum check: the legal minimum is “≥N transmissions per period”, and because B2Brouter batches one ledger per calendar day (§4.2), the minimum is satisfied iff reportable events land on ≥N distinct calendar days within the statutory window (counting distinct
reported_atdates / distinctledger_ids). When real activity clusters on fewer than N days, the count would be under the minimum even though every event is reported — so Green-Got forces a heartbeat transmission (an empty/nil tax report) on additional days to reach N. The reconciliation sweep therefore checks two things: (a) no in-scope operation is unemitted/unacknowledged at the cutoff, and (b) the distinct-ledger-day count for the period is ≥N, scheduling heartbeat transmissions if not. Gate the exact heartbeat mechanism (does B2Brouter accept a nil tax report as a counted transmission?) on staging item C4 (b2brouter/8_staging_verification_matrix.md). - Alerting window: as the cutoff approaches, Green-Got raises internal alerts for any operation that is in scope but not yet emitted or not yet acknowledged by B2Brouter (
new/errortax reports, unmatched collections under the collection regime), and for a period whose distinct-ledger-day count is below the statutory minimum. - Deadline-breach behavior: if the legal deadline is reached with an unresolved gap, Green-Got marks the affected operations as late/at-risk, surfaces them to the customer (who remains legally responsible), and continues to retry transmission. A breach does not silently drop the obligation — the operation stays queued and is re-emitted, and the breach is recorded for the audit trail. See 8. Archiving and Audit Trail.
- Customer notification: the customer is notified when an operation cannot be reported in time (e.g. a service invoice that was never marked collected), with the action needed to resolve it, because the sanction for non-transmission (see §4.4) falls on the customer.
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.
- E-reporting (défaut de transmission): €250 per missing transmission until 2027-09-01, then €500 per missing transmission thereafter (loi de finances 2026).
- E-invoicing (défaut d’émission de facture électronique): €15 per invoice, rising to €50 per invoice from 2027-09-01.
- Annual cap: each obligation type (transmission and emission) is capped at €15,000 per calendar year.
- Non-designation of an approved platform: if no approved platform is designated, the administration issues a three-month notice; on persistence a fine applies and is renewed every three months until regularised.
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:
- VAT on debits. When VAT is due on the debit (always for supplies of goods — they cannot elect collection — and for services where the provider opted for debits), the chargeable event is the invoice issuance / delivery, not the payment, so no payment data is reported.
- Reverse charge / no seller-collected VAT (autoliquidation and adjacent categories). Payment data exists only where the seller collects VAT — i.e. UNCL5305 category
S(and the trivially-zeroZ). It is not reported forAE(reverse charge — buyer accounts for VAT), nor forK(intra-EU supply, VAT exempt under reverse charge),G(export, free of VAT),E(exempt), orO(outside the scope of VAT): none produce seller-collected VAT, so none trigger the art. 290 A payment-data overlay. - B2C cash already in B2C data. When a B2C cash sale is already captured in the B2C transaction data, the payment is not separately reported as payment data.
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:
- generating the structured e-reporting data for B2C and cross-border B2B from the documents and metadata Green-Got submits;
- batching and transmitting Flux 10 to DGFiP on the company’s cadence;
- the Flux 10 tax-report lifecycle. The DGFiP-expected Flux 10 states are
new → sent → acknowledged → registered, orrefused/error.registered_with_errorsis not a DGFiP status — keep it only in generic (non-France) B2Brouter handling, never as an expected France state unless staging proves it. (Flux 1 additionally hasannulled.)
What Green-Got must supply:
- the AR transaction metadata for B2C and cross-border sales that do not flow as domestic B2B e-invoices (counterparty nature, taxable amounts, VAT rates, operation category — items 1–3 of §3);
- the AP acquisition metadata for reportable purchases from non-French suppliers (intra-EU acquisitions and France-located purchases — items 4–5 of §3). These originate from the bills domain, not the invoicing domain, and must be surfaced to e-reporting just like sales;
- the
PaymentCollectedledger entries that are the source of payment data — Green-Got is the only party that knows when a payment was actually collected, because B2Brouter performs no payment processing. Green-Got generates payment links on its own rails, observes the incoming transaction (TransactionMatchedon full settlement), and writes the bank-levelPaymentCollectedledger entry. From that entry, when reportable, the payment data is emitted on the channel-appropriate carrier (decision D5): for domestic B2B invoiced operations (Flux 1) by enriching the invoice’sEncaissée/212 status with the MEN (collection date + amount by VAT rate); for non-invoiced operations through the separate global Flux 10 payment-data flow. Payment data is an overlay, not a Flux-10-only flow. See §3.3, 5. Lifecycle Statuses, and the invoicing transaction-matching documentation. The collection events feed the payment-allocation / VAT-collection model in the PA lifecycle & integration docs. - the company’s VAT regime (debits vs collection) per transaction, so that payment data is emitted only when the collection regime applies.
[gate — staging L-4 / P-7]The issuedmark_as paidtransition is documented (the API reference listspaidas a valid issued target; the France DGFiP guide maps the observedallegedly_paidto 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 thepaid/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:
- Large enterprises / ETI (intermediate-sized) acting as sellers/providers: from 2026-09-01.
- Everyone else (micro / TPE / PME, regardless of size for buyers): from 2027-09-01. This includes all non-established taxpayers as buyers/customers liable for VAT (reverse-charge transactions and intra-EU acquisitions), and — under the August-2025 simplification — all non-established taxpayers regardless of size classification have their obligation postponed to 2027-09-01.
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
- 1. Reform Overview — the three-layer classification (channel, payment-data overlay, provider).
- 5. Lifecycle Statuses — the AFNOR
Encaissée(212) legal status, which (enriched with the MEN) carries the payment data for invoiced operations, and thePaymentCollectedpayment-allocation / VAT-collection model that feeds it. - 8. Archiving and Audit Trail — retention of reported data.
- B2Brouter — DGFiP / PPF registration — enabling Flux 10.
- B2Brouter — sending invoices — automatic Flux 1 / Flux 10 handling.
- Invoicing — transaction matching — matching incoming payments to invoices (
TransactionMatched, full settlement), upstream of thePaymentCollectedledger entry that payment data is derived from.
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.
- CGI art. 289 bis (domestic e-invoicing obligation): https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000053546660
- CGI art. 290 (e-reporting transaction operations, incl. acquisitions): https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000053546668
- CGI art. 290 A (payment-data obligation, art. 289 bis + 290, reverse-charge exclusion): https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000053546674
- B2Brouter DGFiP e-invoicing & e-reporting guide (Flux 10 scope, daily ledger batching, tax-report lifecycle): https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
- E-invoicing vs e-reporting overview: https://ecosio.com/en/blog/e-invoicing-and-e-reporting-in-france/
- E-reporting scope, categories, and payment data: https://www.cleartax.com/fr/en/e-reporting-france
- E-reporting launch for B2C and cross-border (Sept 2026 start): https://www.vatupdate.com/2025/10/22/france-launches-e-reporting-for-b2c-and-cross-border-b2b-transactions-starting-september-2026/
- Foreign companies without a permanent establishment: https://www.impots.gouv.fr/internationalenbusiness/e-reporting-foreign-companies-without-permanent-establishment-france
- September 2026 simplification measures (cadence, removal of daily B2C count): https://www.ey.com/en_gl/technical/tax-alerts/french-government-announces-simplification-measures-as-part-of-september-2026-e-invoicing-mandate
- B2Brouter DGFiP / Flux 1 / Flux 10 handling: https://developer.b2brouter.net/docs/dgfip
- Payment-data e-reporting carried by the enriched
Encaissée/212 status (MEN), incl. partial collection: https://www.impots.gouv.fr/sites/default/files/media/1_metier/2_professionnel/EV/2_gestion/290_facturation_electronique/fiches_reforme/fiche-e-reporting_paiements.pdf - E-reporting de paiement vs e-reporting de transaction (
Encaisséestatus carries payment data; each partial collection is its own transmission): https://qonto.com/fr/blog/experts-comptables/facture-electronique/e-reporting-paiement-e-reporting-transaction-differences - RSI abolished 2027-01-01, replaced by the régime simplifié de déclaration (LF 2025 art. 38): https://www.secob.fr/nos-publications/suppression-du-regime-reel-simplifie-de-tva-a-compter-de-2027-lf-2025/ and https://www.legifiscal.fr/actualites-fiscales/4052-loi-finances-2025-reforme-regime-simplifie-tva-compter-2027.html and https://www.entreprises.cci-paris-idf.fr/fiches-pratiques/tva-le-regime-simplifie-de-declaration
- Sanctions (e-reporting €250→€500/transmission from 2027-09-01; e-invoicing €15→€50/invoice; €15,000 annual cap; non-designation penalty), LF 2026: https://www.legifiscal.fr/actualites-fiscales/4538-facturation-electronique-risque-sanction.html and https://www.fiducial.fr/facturation-electronique/faq/sanctions-non-conformite-obligation-facturation-electronique
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.
- Archiving (archivage): the durable, integrity-preserving retention of invoices and supporting evidence for the legally mandated period, in a form usable by the tax authority on request.
- Piste d’audit fiable (PAF / reliable audit trail): an unbroken evidence chain linking each invoice to the underlying transaction it documents, with timestamps, actors, recorded data changes, and integrity checks. One of the three legal methods for guaranteeing invoice authenticity, integrity, and legibility.
- QES (qualified electronic signature): a qualified electronic signature under eIDAS that guarantees integrity and origin of the invoice. One of the three legal methods.
- EDI (Electronic Data Interchange): a structured exchange method that, when accompanied by an audit trail meeting the regulatory criteria, is one of the three legal methods.
- SAE (Système d’Archivage Électronique): a certified electronic archiving system providing integrity, traceability, and durability guarantees, conforming to NF Z42-013 and NF Z42-029.
- NF Z42-013: the French (and ISO 14641) standard specifying the technical and organizational measures for the design and operation of an electronic archiving system that preserves integrity and legibility of stored documents.
- NF Z42-029: the French standard specifying requirements for a trusted third-party archiver (tiers-archiveur) operating an SAE on behalf of others.
- WORM (Write Once Read Many): a storage property that prevents modification or early deletion of stored objects, used as a tamper-evidence control.
- document_vault: the sibling Green-Got crate providing the customer-facing digital safe (coffre-fort numérique), per-category retention governance, and a pluggable archival-provider abstraction. It is the retention-governance / coffre-fort for the artefacts Green-Got actually stores, not a generic archive for everything. See its
readme.md. - Core S3 bucket: the project’s primary object store. Retained document artefacts live in a dedicated subpart of this bucket (e.g.
s3://<core-bucket>/bills/...), divided intobills/and other areas. This is where stored artefacts (incoming bills, signed quotes) physically reside;document_vaultgoverns retention over them. - Archiving service (ggbs): Green-Got’s own durable archiving service backed by the core S3 bucket. It is the durable-storage layer for the artefacts Green-Got actually keeps (incoming bill originals, signed-quote artefacts). Durable storage is Green-Got’s service — not a third party’s;
document_vaultprovides the retention governance on top of it. - On-the-fly generation: rendering a document (PDF / Factur-X) from immutable structured DB data at the moment it is requested. Used as a display convenience (previews, unsigned renders, and a fallback view) — never as the compliance archive. A regenerated file is not the legal artefact (see §5.2).
- Legal artefact: the exact byte sequence that constitutes the invoice’s legal evidence — the PA-generated / transmitted file for an issued invoice (B2Brouter’s
download_legal_url), or the original received supplier file for an inbound bill. The legal artefact is stored verbatim and content-hashed; it is what an auditor receives. - Content hash: a cryptographic digest (e.g. SHA-256) of a stored artefact’s bytes, recorded at retrieval time, proving the stored copy has not been altered and matching it against the provider’s record.
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.
| Basis | Duration | Starting point | Purpose | Lawful basis |
|---|---|---|---|---|
| (a) VAT (fiscal) retention | 6 years (statutory) | From the invoice date / end of the operation | Tax authority’s right to audit VAT | Legal obligation (BOFiP — 6-year fiscal) |
| (b) Accounting (commercial) retention | 10 years (statutory) | From the end of the fiscal year | Commercial-code bookkeeping retention | Legal obligation (BOFiP — 10-year accounting) |
| (c) Green-Got product archive promise | Customer lifetime with Green-Got, after (a)/(b) elapse | End of the statutory window | Customer-facing “always retrievable” product feature | Separate — needs its own purpose + lawful basis + access controls + deletion/export/offboarding policy + customer controls (see below); not “legal obligation” |
| (d) Permanent non-PII audit evidence | Permanent | Retrieval / event time | Tamper-evidence and PAF integrity proof | Legitimate 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:
- the link from invoice to the originating transaction and supporting documents (order, delivery note, payment);
- timestamps for each step in the document’s life;
- the actors who performed each step;
- any data changes, recorded rather than overwritten;
- integrity checks (for example checksums) proving the stored document has not been altered.
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).
| Method | Basis | How integrity is guaranteed |
|---|---|---|
| QES | eIDAS qualified electronic signature | A qualified signature on each invoice cryptographically proves origin and integrity. Heavy: every invoice must be signed and the signed bytes retained verbatim. |
| EDI | Structured EDI exchange with controls | The structured exchange plus a conforming audit trail and the prescribed EDI controls demonstrate the properties. |
| PAF | Reliable 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 class | Direction | The stored legal artefact | Where it lives | Regeneration (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-Got | Stored in the core S3 bucket, bills/ subpart; document_ref points to it | n/a — served directly from S3 by document_ref |
| Issued invoice | OUTBOUND (AR, invoicing) | The exact transmitted / PA-generated file fetched from B2Brouter (download_legal_url), content-hashed, with provider/audit metadata — the authoritative legal archive | Stored in the core S3 bucket alongside its immutable structured DB data and the PAF | A preview/fallback view may be rendered on the fly from DB data, but it is not the legal copy delivered to an auditor |
| Signed quote | OUTBOUND (invoicing) | The signed PDF + signature audit trail — a signature binds one specific rendered byte sequence; it cannot be regenerated identically | Stored as artefacts in the core S3 bucket | n/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.
| Field | What it holds | Why it is required |
|---|---|---|
legal_artifact_ref | The storage reference (core S3 key / document_vault handle) to the exact transmitted / PA-generated legal artefact, stored verbatim | Locates the authoritative legal bytes — the file delivered to an auditor |
content_hash | Cryptographic digest (e.g. SHA-256) of the stored artefact’s bytes | Proves the stored copy is intact and matches the provider’s record (integrity) |
provider_invoice_id | B2Brouter’s invoice id for the transmitted document | Ties Green-Got’s record to the PA’s record; enables re-fetch and reconciliation |
retrieval_timestamp | The instant the legal artefact was fetched and hashed | Anchors the content hash in time; provenance of the stored copy |
legal_download_path | The provider download path for the legal file — B2Brouter download_legal_url, served by Green-Got as /invoices/{id}/as/legal | The canonical retrieval path for the issued legal artefact |
validation_version | The validation ruleset version in effect at transmission | Makes the artefact’s provenance reproducible; lets a later display render be compared |
rendering_version | The renderer version in effect at transmission | Same — distinguishes the transmitted bytes from any later regeneration |
audit_metadata | Provider/audit metadata bundle: request id, tax report id, CDAR / AFNOR status history, validation output | The 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:
-
Retrieval SLA — the guaranteed availability and latency for fetching a legal artefact (
download_legal_url) on demand, sufficient to meet the 30-day tax-authority accessibility obligation (§6). -
Export rights — Green-Got’s contractual right to bulk-export every transmitted legal artefact and its metadata at any time, in open formats.
-
Exit plan — the procedure for repatriating all legal artefacts to Green-Got’s own archiving service if the B2Brouter relationship ends, with no loss of legal evidence.
-
Checksum strategy — content hashes recorded at retrieval and re-verifiable against the provider copy, so a fetched artefact can be proven identical to what was transmitted.
-
Customer access — how a customer (and an auditor on the customer’s behalf) reaches the stored legal artefact, independent of provider availability.
-
CDAR / status-record retention — B2Brouter’s contractual guarantee to retain the CDAR receipts and AFNOR lifecycle-status history (the outbound PAF evidence originating provider-side) for at least the statutory window, so the PAF can be re-verified after those records age out of Green-Got’s hot path (see §5.5).
-
PA-exit transfer of PAF evidence — on termination of the B2Brouter relationship or a PA switch, the contractual obligation to hand over the full PAF evidence set (CDAR receipts, status timelines, provider invoice ids, tax-report ids) in a re-importable form, so Green-Got’s evidence chain reconstructs intact on the successor PA. This is the evidence-side counterpart to the §5.7 export package.
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 verify | Pass condition |
|---|---|---|
| 1 | Retrieval SLA | Documented availability/latency for download_legal_url sufficient to meet the 30-day tax-authority obligation (§6). |
| 2 | Export rights | Contractual right to bulk-export every transmitted legal artefact + metadata at any time, in open formats. |
| 3 | Exit plan | A defined repatriation procedure on relationship end, with no loss of legal evidence. |
| 4 | Checksum strategy | Content hashes recorded at retrieval, re-verifiable against the provider copy. |
| 5 | Customer access | A path for the customer (and an auditor on their behalf) to reach the artefact independent of provider availability. |
| 6 | CDAR / status-record retention | Provider-side retention of CDAR + AFNOR status records for ≥ the statutory window. |
| 7 | PA-exit PAF-evidence transfer | Contractual 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
PASSwith recorded evidence. This table is referenced as the closure home for the staging-matrix offboarding archive/export item (b2brouter / 8. Staging Verification Matrix F4).
| # | Term | Evidence link | Owner | Verified (date) | Status |
|---|---|---|---|---|---|
| 1 | Retrieval SLA | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 2 | Export rights | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 3 | Exit plan | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 4 | Checksum strategy | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 5 | Customer access | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 6 | CDAR / status-record retention | ‹UNFILLED — link to contract clause / evidence› | ‹UNFILLED — legal owner› | ‹UNFILLED — YYYY-MM-DD› | OPEN (unverified) |
| 7 | PA-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:
- Legal artefacts + content hashes — every stored issued-invoice legal artefact, incoming bill original, and signed-quote artefact, each with its
content_hashfor re-verification after restore. - Provider invoice ids (
provider_invoice_id) — the B2Brouter identity of each transmitted document, so artefacts can be re-correlated with provider records. - Tax-report ids — the e-reporting / tax-report identifiers tying invoices to what was reported to PPF/DGFiP.
- Lifecycle / CDAR status history — the full AFNOR / CDAR status timeline per document (the PAF evidence chain).
- Mandates (MandateEvidence) — the audit-ready mandate evidence authorising issuance on each customer’s behalf (see 9. Onboarding §4.5).
- Enrollment state — each customer’s PA-registration / onboarding state.
- Pending transmissions — in-flight invoices not yet in a terminal status, so nothing is dropped on cutover.
- Request ids — the provider
X-B2B-API-Request-Id/ request log references that make each operation traceable. - Reconciliation state — the matched-transaction (order ↔ invoice ↔ payment) state and any open reconciliation items.
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).
- Provider-exit (PA switch / B2Brouter failure / forced repatriation) is already covered by the rehearsed export package in §5.7 and the §5.6 exit terms. It moves artefacts between providers; it does not delete customer data.
- Customer-exit (account closure / unsubscribe / a post-statutory erasure request) is the path §5.7 does not cover and is defined here. It governs what the customer receives and what is deleted when the relationship ends.
Customer-exit flow (account closure / unsubscribe).
- 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).
- 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.
- 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:
- Customer self-service (13. Privacy §3.1) — the customer reaching their own archive.
- Auditor share link — a third-party auditor reaching a customer’s archive on the customer’s behalf, via a per-token, TTL-bound, revocable share link the customer (or Green-Got under instruction) mints.
- Tax-authority access — Green-Got responding to a DGFiP request as the responsible party for the customer’s archive, which the customer may not have initiated. Because the legal obligation to produce stays with the issuing company (§7), the protocol is:
- 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).
- 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. - 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.
- 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
- 3. Actors and Legal Posture — liability split between the company, the PA, and Green-Got.
- 5. Lifecycle Statuses — the status history that feeds the audit trail.
- 7. E-Reporting — retention of reported data.
- 10. Integration Contracts —
document_reffor the inbound original; where stored artefacts are referenced. - 13. Privacy and Data Protection — the split between statutory legal-artefact retention, the separate product archive promise, and CNIL-aligned operational / PII retention, DSAR / right-to-erasure handling, and log minimisation.
document_vault(sibling cratereadme.md) — retention governance, WORM, and the archival-provider abstraction for stored artefacts.
9. Sources
- Retention periods, PAF, three methods, and SAE (NF Z42-013 / NF Z42-029): https://www.fiscal-requirements.com/news/5556
- AFNOR guidance on electronic invoice archiving (FD Z42-029): https://www.vatupdate.com/2026/05/29/france-2026-e-invoicing-reform-afnor-guidance-on-electronic-invoice-archiving-fd-z42-029/
- BOFiP — the three equivalent methods (QES / EDI / PAF) for guaranteeing authenticity, integrity, legibility; QES is presumed compliant and dispensed from documenting a PAF; PAF is the reliable-audit-trail route: BOI-TVA-DECLA-30-20-30-10 (https://bofip.impots.gouv.fr/bofip/8862-PGP.html/identifiant=BOI-TVA-DECLA-30-20-30-10-20180207) and BOI-TVA-DECLA-30-20-30-50 (https://bofip.impots.gouv.fr/bofip/8869-PGP.html/identifiant=BOI-TVA-DECLA-30-20-30-50-20180207).
- AFNOR NF Z42-013 (= ISO 14641-1) — probative-value electronic archiving (integrity, traceability, immutable chain of custody, SHA-256 fingerprint/sealing/journaling); a voluntary standard supporting evidentiary admissibility, not a regulatory precondition for VAT-compliant archiving: https://www.afnor.org/en/decryptions/cybersecurity/electronic-archiving-faq/
- BOFiP — obligation to retain invoices in their original form, format, and full content with audit evidence (BOI-CF-COM-10-10-30): https://bofip.impots.gouv.fr/bofip/645-PGP.html/identifiant%3DBOI-CF-COM-10-10-30-20250903 ; and BOI-CF-COM-10-10-30-10.
- B2Brouter — fetching the final transmitted legal artefact for archival (
download_legal_url, send-invoices-end-to-end): https://docs.b2brouter.net/en/developers/common-use-cases/send-invoices-end-to-end/
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:
- Enrollment: the end-to-end process that takes a Green-Got business customer from “no e-invoicing capability” to “registered in the Annuaire, mandate captured, able to issue and receive e-invoices through B2Brouter”.
- mandat: the legal authorization by which a company empowers Green-Got (as SC) and B2Brouter (as PA) to act on its behalf (issue, receive, e-report, manage lifecycle statuses). The company remains legally responsible for invoice content and compliance; the PA is responsible for transmission, format conformance, and the audit trail. See 3. Actors and Legal Posture.
- mandate consent record: the Green-Got-owned record that captures the customer’s grant of the mandat. B2Brouter has no explicit mandate object (authorization there is account-level: owner/admin roles), so Green-Got is the system of record for consent. Stored at the organisation level:
organisation_id, granted scope,accepted_at, consent version (the exact contract text/version shown), and who accepted (the natural person and their authority to bind the company). It is revocable and is revoked/superseded on PA switch. Part of the PAF. See §4.1. - eDocSync: B2Brouter’s white-label provisioning model, in which Green-Got creates and manages B2Brouter accounts on behalf of its end customers through the REST API (as opposed to eDocExchange, where the end user self-provisions through B2Brouter’s own web UI). Green-Got uses eDocSync. See B2Brouter — Onboarding accounts.
- B2Brouter account: the company-level record in B2Brouter that represents one enrolled French legal entity. One account per SIREN; additional establishments (SIRETs) of the same legal entity are organizational units within that single account, not separate accounts. See B2Brouter — Onboarding accounts.
b2brouter_account_id: the identifier B2Brouter returns when an account is created. The current account id is held at the organisation / enrollment level (one per SIREN) and is the value used to act on the customer’s behalf. APaTransmissionadditionally captures the account id as an immutable historical snapshot at the moment of transmission (for audit, replay, and migration) — the snapshot records which account actually carried that transmission and never changes, even if the organisation’s current account id later changes. The organisation/enrollment value is the live link; the per-transmission value is a frozen historical fact. See 10. Integration Contracts.- MandateEvidence: the Green-Got-owned, audit-ready evidence bundle proving that a valid mandat authorises each regulated flow. It is the superset of the mandate consent record and the surrounding proof artifacts (signed designation, contract text + hash, signatory authority/KYB evidence, consent capture metadata, scope flags, registration/account proofs, and the revocation/supersession trail). Regulated flows are gated on explicit mandate/designation states derived from MandateEvidence, not on subscription status. See §4.5.
- tax report settings (
dgfip): the B2Brouter per-account configuration object that, when enabled, triggers automatic PPF Annuaire registration and activates Flux 1 (domestic B2B e-invoicing) and Flux 10 (e-reporting). See 7. E-Reporting. - Routing scope: the level at which a company is addressed in the Annuaire — whole legal entity (SIREN), specific establishment (SIRET), or an internal routing code. At Green-Got MVP this is SIREN-only (
0225:SIREN), applied automatically at enrollment; SIRET / internal-code scope are DISABLED future states gated behind uncertainty P-2. See 6. Annuaire and Routing and §4.4. - OrganisationId: the Green-Got identifier of the enrolled business, owned by the
organisationcrate (which also owns KYB). The mandate andb2brouter_account_idhang off the organisation/enrollment record.
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.
| Step | Name | Actor that drives it | What it produces | Legal/technical effect |
|---|---|---|---|---|
| (a) | Mandate UI | Customer (consents) in the Green-Got app | Captured, 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 creation | Green-Got plateforme_agreee via eDocSync (POST /accounts) | b2brouter_account_id (one per SIREN) | The company exists as a sender+receiver in B2Brouter |
| (c) | Enabling DGFiP | Green-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:
- The identity of the granting company (legal name, SIREN, and the establishment SIRET(s) covered).
- The scope of the authorization: that B2Brouter (the certified PA), provisioned and operated through Green-Got (the SC), is authorized to issue invoices on the company’s behalf, receive invoices addressed to it, transmit e-reporting data (Flux 10) to DGFiP, and manage lifecycle statuses on its behalf.
- An acknowledgement that the company remains legally responsible for the content and compliance of its invoices and for archiving obligations, while the PA is responsible for secure transmission, format conformance, and the audit trail. See 3. Actors and Legal Posture and 8. Archiving.
- The identity of the natural person consenting on behalf of the company and their authority to bind it (tied to the organisation’s KYB record).
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:
| Field | Meaning |
|---|---|
organisation_id | The granting company (one mandate per French legal entity / SIREN). |
scope | What is authorized: issue, receive, e-report (Flux 10), and manage lifecycle statuses on the company’s behalf. |
accepted_at | Consent timestamp. |
consent_version | The exact contract/consent text + version shown to the customer (so the precise wording accepted is reproducible). |
accepted_by | The natural person who consented (authenticated user id). |
signatory_role | The role under which that person bound the company (e.g. legal representative, authorised signatory) — the claimed authority, captured at consent. |
kyb_reference_id | A 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_at | When that KYB authority was verified, for auditability of the capture moment. |
status | active → revoked / 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 latestactiveconsent governs. Revocation implies winding down the enrollment, and a PA switch marks the recordrevoked/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:
superseded— a continuity transition: a newactiveconsent record immediately replaces the old one with no gap in authorization. Triggered by Green-Got/the customer re-consenting (material contract-version change, re-grant, or a per-flag MandateEvidence change — §4.5). The superseding record’s id is recorded in the prior record’s revocation trail. Authorization is continuous; in-flight flows keep running under the new record.revoked— a withdrawal transition: authorization ends and is not immediately replaced. Triggered by the customer withdrawing the mandate, or by a PA switch away from Green-Got (§5.4) where Green-Got stops acting as issuer. There is no successoractiverecord.
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.
- Endpoint:
POST /accounts(hosthttps://api.b2brouter.net, staginghttps://api-staging.b2brouter.net), authenticated with theX-B2B-API-Keyheader. See B2Brouter — Onboarding accounts. - One account per SIREN. The
cin_value(company identifier —cin_scheme 0002for SIREN,0009for SIRET; nottin_*, which carries the FR VAT number undertin_scheme 9957) may be a SIREN or a SIRET; B2Brouter extracts the SIREN and resolves to a single account. Additional establishments (other SIRETs of the same legal entity) become organizational units within that account, not new accounts. - Required fields (within the
accountobject):country,cin_scheme(0002SIREN /0009SIRET),cin_value(SIREN or SIRET — the legal-identity/company number),name,address,city,postalcode,email,rounding_method. The FR VAT number, when present, is carried separately astin_scheme(9957) +tin_valueand is never where the SIREN/SIRET lives. See B2Brouter — Onboarding accounts for the authoritative field list. - Response: the created account, including the identifier Green-Got persists as
b2brouter_account_id. Error responses: 400 (invalid params), 401 (auth), 422 (validation, includingtakenwhen an account already exists for that SIREN/CIN).
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.
- Endpoint: the DGFiP tax-report setting is created with
POST /accounts/{account}/tax_report_settings(sending the fulltax_report_settingbody — never a bare{code, enabled}toggle) and updated withPUT /accounts/{account}/tax_report_settings/dgfip(also full body). The canonical flow and the exact body are documented once in B2Brouter — Onboarding accounts §4; see also 7. E-Reporting. - Effect. B2Brouter automatically registers the SIREN/SIRET in the PPF Annuaire, making the company discoverable as routable to B2Brouter, and activates:
- Flux 1 — domestic B2B e-invoicing (issue and receive structured invoices). See 2. Platform Architecture.
- Flux 10 — e-reporting of B2C and cross-border transactions and payment data to DGFiP. See 7. E-Reporting.
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):
0225:SIREN— the whole legal entity (all establishments routed to one platform). The only scope Green-Got declares at MVP.0225:SIREN_SIRET— a specific establishment. DISABLED / future (gated behind uncertainty P-2).- An internal routing code — finer department-level routing within an establishment. DISABLED / future (gated behind uncertainty P-2).
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:
- MVP: SIREN-level only (
0225:SIREN, the whole legal entity). This matches B2Brouter’s one-account-per-SIREN model — a single account receives for the entire entity — registers automatically when thedgfiptax-report setting is enabled, and needs no extra provider call. There is no user-selectable scope field at MVP and no SIRET/internal-code choice is captured or persisted. - SIRET-level (
0225:SIREN_SIRET) and internal-code scope are DISABLED future states. They require an as-yet-undocumented B2Brouter endpoint/field to convey the chosen granularity at account-configuration time, gated behind uncertainty P-2. They are not offered at enrollment, not captured, and not persisted until P-2 is closed.
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).
| Field | Meaning |
|---|---|
designation_artifact | The 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_hash | The 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_authority | Evidence that the consenting natural person had authority to bind the company — tied to the organisation’s KYB record (role, representative status, KYB reference). |
consent_capture | The 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_flags | Explicit 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_proof | Proof that PPF Annuaire registration was performed under this mandate (the DGFiP enablement result / registration confirmation returned via B2Brouter). |
b2brouter_account_proof | Proof of the B2Brouter account and its tax-setting (dgfip) state created/enabled under this mandate (account id snapshot + tax-report-setting result). |
revocation_trail | The revocation / supersession history: each state transition (active → revoked / superseded), with timestamp, actor, and the id of the superseding MandateEvidence record. Never deleted. |
retention_export_policy | The 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:
| Flow | Required 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 reception | MandateEvidence 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 submission | MandateEvidence 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.
- Capture & defaults at enrollment. The mandate is captured as a single bundled in-app consent (one checkbox + the accepted TOS/contract clause — consent version
pa-mandate-v1; see Uncertainties L-1), not as six independent opt-ins. Accepting that bundled consent sets all sixscope_flagstotrueat enrollment (issue,receive,e_report,payment_data,archive,support). There is no granular per-flag UX at MVP; the flags exist so each regulated flow can be gated and individually revoked, not so the customer toggles them one-by-one at sign-up. - Per-flag revoke / re-enable. A flag may later be set to
false(e.g. a customer that revokes only e-reporting, or a scope that becomes unavailable). Flipping a flag is a MandateEvidence state change: it captures a new append-only MandateEvidence version (the prior one issuperseded, never overwritten — see the revocation trail), records the actor and timestamp, and takes effect immediately for gating. Re-enabling a flag follows the same append-only path. Flipping all flagsfalse, or revoking the whole mandate, transitions the consent record torevoked(§4.1). - Single source of truth. The flags live on MandateEvidence (data model); the consent record’s
status(active/revoked/superseded) and the flags together determine what may proceed. Subscription state never substitutes for a flag.
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 state | Triggered by | Class | Policy |
|---|---|---|---|
KybBlocked | The 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). |
AccountFailed | POST /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). |
DgfipEnablementFailed | POST/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. |
retry_attempt/max_retry. The enrollment record carries a per-stepretry_attemptcounter and a configuredmax_retryceiling. A retry advances from the first incomplete step (it never re-runs completed steps). Reachingmax_retryon a transient failure promotes it to a terminal failure state; terminal failures require manual/ops intervention and surface a mandatory enrollment-failure alert (symmetric with the loss-of-designation alert in §5.5).- Rollback. No partial enrollment is silently abandoned. A failed step leaves the enrollment parked in its failure state with the partial artefacts intact (e.g. an
AccountCreatedorg that hitDgfipEnablementFailedkeeps itsb2brouter_account_id); resumption re-enters at the failed step rather than recreating prior steps. The state machine is the source of truth for which step to resume — see 12. Data Model for the persisted Enrollment entity and its status enum.
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 inuncertainties.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:
- Account missing / no id held → re-enter at step (b): create-or-adopt (§5.1).
- Account exists but
dgfipnot enabled → re-enter at step (c): re-enabledgfipidempotently (re-POST/PUTtax_report_settings). - Account exists and
dgfipalready enabled → the enrollment is effectively complete; advance the state machine toEnrolledwithout re-issuing provider calls (re-enable is a no-op anyway). - Account exists but in an unexpected provider state (suspended/closed on the provider side, or a mismatched CIN) → do not auto-resume; treat as the controlled-reprovisioning edge case (§5.3) and surface for review.
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).
- The new (gaining) PA drives the change: the customer signs a mandat de désignation, and the new PA updates the customer’s routing entry in the PPF Annuaire. There is no formal de-registration — the Annuaire entry is simply overwritten to point at the new PA.
- The old (losing) PA must acknowledge the change within 5 business days; silence counts as tacit acceptance (it cannot block the switch).
- The old PA must maintain a minimal service for a transition window (redirect residual flows that still arrive at it, keep archive access available) and keep archives for 10 years. The legal basis for this minimal-service window is PLF 2026 art. 28 (renumbered art. 123 in the adopted text), which extends the losing-platform minimal service from 6 to 12 months (amendment I-880 of 27 November 2025); the exact modalities must be specified by decree and are not yet final.
[open — legal](losing-PA minimal-service-window modalities, pending the final decree under PLF 2026 art. 28 / art. 123 — see Uncertainties L-2). - Identifier portability is guaranteed by the Annuaire — the customer keeps its SIREN/SIRET routing identifiers; only the PA they resolve to changes.
- The Annuaire holds exactly one current PA per identifier — “last registration wins”. When the customer designates another PA, B2Brouter closes our Annuaire entry; we are no longer their PA from that moment.
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:
- The issuing MANDATE (
scope_flags.issue): the customer’s grant authorising Green-Got (as SC) + B2Brouter (as PA) to issue/transmit invoices on their behalf. This is captured in MandateEvidence (§4.5) and is the legal basis for outbound. - Being the designated PA for INBOUND routing: the customer’s Annuaire entry pointing at our B2Brouter account, so supplier invoices addressed to their SIREN route to us. This is the basis for inbound.
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:
- Inbound stops. Once another PA is designated for that SIREN in the Annuaire, the inbound flux stops: we no longer receive INCOMING bills (the customer’s supplier invoices) for them, because the Annuaire now routes their identifier to the other PA, not to our B2Brouter account. Inbound requires being the designated PA; we are not.
- Outbound continues while the issuing MANDATE is valid AND the subscription is active. The customer can still use Green-Got to send (issue/transmit) invoices — outbound does not require being the designated inbound PA. It requires a valid issuing mandate (MandateEvidence
status = activewithscope_flags.issue) AND an active business-account subscription. The subscription is the commercial precondition; the mandate is the legal basis — subscription alone is not sufficient. The customer being represented by another PA for their own inbound does not block us sending on their behalf as the issuer, but a revoked issuing mandate does.
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:
- Proactive detection (preferred): a periodic Annuaire/directory lookup sweep over active enrollments (B2Brouter
GET /directory/.../ the DGFiP status on the account). When the customer’s identifier no longer resolves to our B2Brouter account, designation has been lost and inbound for that SIREN will no longer reach us. - No push notification. There is no documented push (“you have lost designation”) from the PPF or B2Brouter, which is why the proactive sweep is the primary detection mechanism.
Note — internal error model. The internal error model (see 5. Lifecycle Statuses §1.1) carries a dedicated
not_designated_pavariant. 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:
- Cadence: at least daily (≥ once per 24h). Because reception is universal from 2026-09-01, a silent loss must be caught within roughly a day, not a billing cycle.
- The sweep depends on resolving the open
not_designated_pasignal (the exact B2Brouter/Annuaire code for “no longer the designated PA” — see the note below and Uncertainties); until that code is confirmed, the sweep relies on “identifier no longer resolves to our account” as the detection condition. - The sweep is wired as a Temporal workflow in 14. Implementation Wiring §6 (a sibling document owns that table).
Design rule — the stand-down process. On detecting loss of designation, Green-Got:
- records that inbound has moved away (the customer’s identifier now resolves to another PA), timestamped — persisted as
inbound_designation_lost_aton the enrollment (see 12. Data Model); - stops expecting inbound for that SIREN — supplier invoices will no longer arrive through Green-Got;
- keeps outbound issuance available for as long as the issuing mandate is valid (MandateEvidence
status = activewithscope_flags.issue) and the business-account subscription is active — it is gated on the mandate and the subscription, not on inbound PA-designation; - retains archives + audit trail for the full retention period (see 8. Archiving §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:
- Alert SLA. The first alert (in-app + email) is dispatched within one sweep cycle of detection (≤ 24h after
inbound_designation_lost_at), targeted within the same day as detection. - Lifecycle: resend-until-acknowledged. A single send is insufficient — email can bounce and in-app banners can go unseen. The alert re-sends on a backoff cadence (e.g. day 0, then periodically) until the customer acknowledges it, then stops. It is never silently dropped after one attempt.
- Acknowledgement model + persisted entity. Acknowledgement is an explicit customer action (“I’ve seen this — the switch was intended” / “re-designate Green-Got”). Green-Got persists a
DesignationLossAlertrecord (see 12. Data Model) capturing: the enrollment/organisation_id,detected_at(=inbound_designation_lost_at),first_alerted_at, the resend history,acknowledged_at+acknowledged_by, and the acknowledgement outcome (intended switch vs re-designate request). The resend loop reads this record to decide whether to keep alerting. A still-unacknowledged alert past a threshold escalates to ops.
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:
- The mandate (step a) is captured by Green-Got before any B2Brouter call; B2Brouter and the Annuaire are reached only after consent exists.
- Steps (b) and (c) are the only calls into B2Brouter that change State-visible registration; Green-Got never contacts the PPF/Annuaire directly.
b2brouter_account_idis persisted once, at organisation level, and reused for every subsequent issue/receive/e-report operation. See 10. Integration Contracts.- All four steps are idempotent and resumable (§5).
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 item | Level | Notes |
|---|---|---|
| Mandate consent record | Organisation / enrollment | organisation_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 / enrollment | One 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) | PaTransmission | An 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 state | Organisation / enrollment | Whether dgfip tax report settings are enabled (Annuaire-registered). |
| Routing scope | Organisation / enrollment | SIREN-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 status | Organisation / enrollment | Position 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_at | Organisation / enrollment | Timestamp 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 alert | DesignationLossAlert | The 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_flags | MandateEvidence | Per-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
- 1. Reform Overview — scope, calendar, Green-Got’s role, shared terminology.
- 2. Platform Architecture — the 5-corner model, Flux 1 / Flux 10, routing.
- 3. Actors and Legal Posture — the mandat, responsibilities, switching PA.
- 6. Annuaire and Routing — routing keys and scope.
- 7. E-Reporting — Flux 10 obligations activated by DGFiP enablement.
- 8. Archiving — retention and audit trail (PAF).
- 10. Integration Contracts — how enrollment links into the invoicing/bills event contracts.
- B2Brouter — Onboarding accounts —
POST /accounts,POST …/tax_report_settings(create) /PUT …/tax_report_settings/dgfip(update), eDocSync, field lists.
9. Sources
- French reform — mandate, switching PA, retention: https://www.fiscal-requirements.com/news/5556
- Changing approved platform (gaining PA drives the Annuaire update via mandat de désignation; no formal de-registration; 5-business-day acknowledgement / silence = acceptance; losing-PA minimal service; 10-year archives; identifier portability): https://ma-facture-electronique.org/plateforme-agreee/changer-de-plateforme/ ; https://www.getyooz.com/fr/changer-de-plateforme-agreee ; https://www.pennylane.com/fr/fiches-pratiques/comment-changer-de-plateforme-agreee/ ; https://www.impots.gouv.fr/facturation-electronique-et-plateformes-agreees
- PLF 2026 art. 28 (renumbered art. 123) — losing-PA minimal-service window extended 6→12 months (amendment I-880, 27 Nov 2025; modalities decree-pending): https://www.arteva.fr/blog/actualites/article-28-plf-2026-renumerote-article-123/ ; https://www.legifiscal.fr/actualites-fiscales/4351-facturation-electronique-pourra-t-changer-facilement-plateforme-agreee.html
- Platform architecture (PPF directory + concentrator; PA registration): https://www.fonoa.com/resources/blog/france-e-invoicing-architecture
- Annuaire and routing scope declaration before 2026-09-01: https://www.b2brouter.net/global/annuaire-france-invoice/
- Annuaire routing keys (SIREN / SIRET / internal code): https://www.infos-pa.com/articles/annuaire-ppf-routage-siren-plateformes-agreees
- B2Brouter eDocSync,
POST /accounts,PUT …/tax_report_settings/dgfip(auto Annuaire registration, Flux 1 + Flux 10): https://developer.b2brouter.net/docs/accounts_guide and https://developer.b2brouter.net/docs/dgfip - B2Brouter create-account reference: https://developer.b2brouter.net/reference/create-account
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
- 1. Overview and Concepts — B2Brouter’s role, deployment zones, and domain objects (accounts, contacts, invoices, tax report settings, identifiers); eDocExchange vs eDocSync.
- 2. Authentication and Environments —
X-B2B-API-Key/X-B2B-API-Version, base URLs, environment-specific keys. - 3. Onboarding Accounts —
POST /accounts(one account per SIREN),PUT …/tax_report_settings/dgfip→ PPF annuaire registration. - 4. Sending Invoices — contact → invoice →
send_invoice→ retrieve files; routing; Chorus Pro (B2G); Flux 1 / Flux 10; payment fields (metadata only). - 5. Receiving Invoices — reception channels, listing
ReceivedInvoice, fetching the original + structured data. A received invoice becomes abills/document. - 6. Statuses and Webhooks — Green-Got’s supported subset of B2Brouter’s status sets,
mark_as/ack, HMAC-SHA256 webhook verification, events polling fallback, CDAR. - 7. API Mechanics — rate limits and the conservative concurrency throttle, error codes, pagination, idempotency, the query language, attachments, inline invoice validation, the Temporal transmission queue, and operational resilience.
- 8. Staging Verification Matrix — request/response samples to capture on staging and the confirm-before-implementation checklist.
Related
- Uncertainties — documented gaps in B2Brouter’s public API (webhook payload schema, idempotency, field lists, French error codes) to confirm on staging.
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
- PA (Plateforme Agréée): State-registered, certified third party that is the sole intermediary for B2B e-invoice exchange and e-reporting under the French reform. B2Brouter is a certified PA.
- SC (Solution Compatible): optional, non-certified software layer (ERP / invoicing SaaS) that works through a PA. Green-Got is an SC on top of B2Brouter.
- Peppol Access Point: a node on the Peppol network able to send and receive documents for connected senders/receivers. B2Brouter is a Peppol Access Point.
- PPF (Portail Public de Facturation): the State directory (Annuaire) and data concentrator. It no longer exchanges invoices. See 1. Reform Overview.
- Annuaire: the PPF central directory of in-scope French assujettis / Annuaire-registered entities (SIREN/SIRET) and their chosen PA, used for routing.
- SIREN: 9-digit French legal entity identifier.
- SIRET: 14-digit establishment identifier (SIREN + 5-digit NIC).
- DGFiP: Direction Générale des Finances Publiques, the French tax authority.
- TIN (Tax Identification Number): a tax identifier of an account or contact, carried with a scheme.
- CIN (Company Identification Number): a company/registration identifier (e.g. SIRET, DIR3), carried with a scheme.
- PIN (Peppol Identification Number): a Peppol participant identifier, carried with a scheme.
- Scheme: a code classifying an identifier’s type/issuer (e.g. ISO 6523 ranges
0001–0088, plus8XXXranges). - Transport Type Code: how a document is delivered to a recipient (the channel), e.g.
fr.chorus,peppol,email. - Document Type Code: the document format/profile produced, e.g.
xml.ubl.invoice.chorus. - Flux 1: domestic B2B transmission to the PPF for distribution to the recipient’s PA.
- Flux 10: e-reporting transmission (B2C and cross-border) to the DGFiP.
- eDocExchange: B2Brouter integration model where accounts are managed through the web UI (no account create/delete via API).
- eDocSync: B2Brouter white-label integration model where accounts are provisioned and managed via API on behalf of end customers. Green-Got uses eDocSync.
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:
- registration of the company in the PPF Annuaire,
- document generation (UBL, CII, Factur-X) from Green-Got’s structured invoice data,
- routing of B2B invoices to the recipient’s PA (via the PPF, Flux 1),
- tax reporting to the DGFiP (e-reporting, Flux 10).
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.
| Zone | Purpose |
|---|---|
| Sandbox | Development and early integration. |
| Staging | Larger integrations and pre-production validation. |
| Production | Live 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.
| Concept | Description |
|---|---|
| Identity | Keyed on the company SIREN (extracted from the supplied SIREN or SIRET). |
| Establishments | SIRETs of the same SIREN are org units within the one account. |
| Tax reporting | Configured per account via Tax Report Settings (see 4.4). |
| Transports | An 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 type | Meaning |
|---|---|
| IssuedInvoice | An invoice issued (sent) by the account (outbound, accounts receivable). |
| ReceivedInvoice | An invoice received by the account (inbound, accounts payable). |
| IssuedSelfInvoice | A self-billed invoice issued by the account on behalf of the supplier. |
| IssuedSimplifiedInvoice | A simplified invoice issued by the account. |
| ReceivedSelfInvoice | A 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.
| Identifier | Meaning | Example scheme context |
|---|---|---|
| TIN | Tax Identification Number. | Tax/VAT schemes (e.g. VAT, TAX). |
| CIN | Company Identification Number. | Registration schemes (e.g. SIRET 0009, DIR3). |
| PIN | Peppol Identification Number. | Peppol participant schemes. |
Schemes are drawn from code classifications including ISO 6523 ranges (0001–0088) 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.
0225is a Peppol EAS / transport scheme, not (confirmed) acin_scheme. France’s0225is the EAS code that identifies the French Peppol/Annuaire participant transport (B2Brouter creates a Peppol0225transport when DGFiP is enabled — 3. Onboarding Accounts §4.1). The company-identity schemes on accounts/contacts are0002(SIREN) and0009(SIRET). Whether0225is also accepted as acin_schemevalue (alongside0002/0009) is not documented and is confirmed/denied on staging (Uncertainties W-1; 8. Staging Verification Matrix). Until then, do not use0225as acin_scheme— treat it strictly as the EAS/transport identifier; the working CIN schemes are0002/0009.
5. Transport Type Code vs Document Type Code
These two codes are independent and both can apply to a recipient or contact:
- Transport Type Code — how the document is delivered (the channel/route).
- Document Type Code — what document/format is produced.
| Example value | Kind | Meaning |
|---|---|---|
fr.chorus | Transport Type Code | Deliver via Chorus Pro (B2G). |
peppol | Transport Type Code | Deliver via the Peppol network. |
email | Transport Type Code | Deliver by email. |
xml.ubl.invoice.chorus | Document Type Code | Produce 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.
| Model | Account management | Use case |
|---|---|---|
| eDocExchange | Accounts 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. |
| eDocSync | White-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
- 1. Reform Overview — reform scope, rollout, Green-Got’s SC posture.
- 4. Formats and Invoice Data — Factur-X / UBL / CII formats and mandatory data.
- 2. Authentication and Environments — auth headers and base URLs.
- 3. Onboarding Accounts — account creation and DGFiP enablement.
8. Sources
- B2Brouter — Introduction: https://developer.b2brouter.net/docs/introduction
- B2Brouter — DGFiP: https://developer.b2brouter.net/docs/dgfip
- B2Brouter — Accounts guide: https://developer.b2brouter.net/docs/accounts_guide
- B2Brouter — Schemes guide: https://developer.b2brouter.net/docs/schemes_guide
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
- API key: secret credential presented on every request via the
X-B2B-API-Keyheader. Environment-specific and group-based. - API version: the dated B2Brouter API contract selected via the
X-B2B-API-Versionheader. - Group: the B2Brouter account group an API key is scoped to (group-based keys).
- CurrentEnvParameters: Green-Got’s runtime configuration/secret access pattern used to load environment-specific parameters such as B2Brouter keys and base URLs.
2. Header Authentication
Every request authenticates with HTTP headers — there is no OAuth flow.
| Header | Required | Value |
|---|---|---|
X-B2B-API-Key | Yes | The environment-specific, group-based API key. |
X-B2B-API-Version | Recommended | API contract date; current default 2026-04-20 (see §3.1). |
accept | Recommended | application/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.
| Environment | Base URL | Key form | Use case |
|---|---|---|---|
| Sandbox | https://api.b2brouter.net | key prefixed test_ | Day-to-day development and early integration. B2Brouter’s recommended starting environment. |
| Staging | https://api-staging.b2brouter.net | staging key | Larger integrations and pre-production validation. Shares prod’s rate-limit profile minus throughput (see 7. API Mechanics §2). |
| Production | https://api.b2brouter.net | production key | Live 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
- 1. Overview and Concepts — deployment zones and domain model.
- 3. Onboarding Accounts — first authenticated calls (account creation).
7. Sources
- B2Brouter — Setting up guide: https://developer.b2brouter.net/docs/setting_up_guide
- B2Brouter — Get accounts: https://developer.b2brouter.net/reference/get-accounts
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
- eDocSync: B2Brouter’s white-label model where Green-Got provisions accounts via API on behalf of customers (see 1. Overview and Concepts).
- Account: a company on B2Brouter, keyed one-per-SIREN.
- CIN: Company Identification Number, carried with a
cin_scheme. For France: the SIREN (cin_scheme = 0002) or the establishment SIRET (cin_scheme = 0009). This is the legal-entity / establishment identity — not the tax number. - TIN: Tax Identification Number, carried with a
tin_scheme. For France: the French VAT number only (tin_scheme = 9957, valueFR…). The SIREN/SIRET is never atin_value. - DGFiP: the French tax authority; the
dgfiptax report setting enables French e-invoicing/e-reporting. - Annuaire: the PPF central directory; enabling DGFiP auto-registers the account there.
- Flux 1 / Flux 10: domestic B2B transmission / e-reporting transmission.
- QAS: the DGFiP qualification/test environment (“Qualification, Acceptance, Service”) used for Chorus Pro testing.
- rounding_method: account-level rounding policy applied to invoice computations.
2. Account Creation (eDocSync)
Green-Got creates a customer account with a single call.
| Verb | Path | Purpose |
|---|---|---|
POST | /accounts | Create 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:
| Field | Required | Description |
|---|---|---|
country | Yes | Account country (ISO 3166-1 alpha-2, e.g. fr). |
cin_scheme | Yes | Company-identity scheme. FR legal entity → 0002 (SIREN); FR establishment → 0009 (SIRET). |
cin_value | Yes | The SIREN (9 digits, scheme 0002) or SIRET (14 digits, scheme 0009). The SIREN is extracted; one account exists per SIREN. |
tin_scheme | No | Tax (VAT) scheme. For France: 9957 (French VAT number). |
tin_value | No | The 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. |
name | Yes | Company name. |
address | No | Street address. |
city | No | City. |
postalcode | No | Postal code. |
province | No | Province / region / department. |
email | No | Contact email. |
phone | No | Contact phone number. |
rounding_method | No | Rounding policy for the account. |
Response codes:
| Code | Meaning |
|---|---|
200 | Account created. |
400 | Invalid request parameters. |
401 | Unauthorized (missing/invalid/expired API key). |
404 | Not found (e.g. the addressed group/subscription is not resolvable for this key). |
409 | Conflict (the account conflicts with existing resource state — see the SIREN dedupe note in 7. API Mechanics §5.1). |
422 | Unprocessable 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:
| Field | Expected constraint (pre-flight) | Provider failure |
|---|---|---|
country | ISO 3166-1 alpha-2 (lowercased, e.g. fr) | parameter_inclusion if not a known country |
cin_scheme | enum: 0002 (SIREN) / 0009 (SIRET) for FR | parameter_inclusion |
cin_value | 0002 → exactly 9 digits + SIREN Luhn validity; 0009 → exactly 14 digits + SIRET Luhn validity | parameter_blank / parameter_inclusion / parameter_taken (duplicate) |
tin_scheme | enum: 9957 for FR VAT | parameter_inclusion |
tin_value | FR VAT format FR + 2 key digits + 9-digit SIREN (FR\d{11}) | parameter_inclusion |
name | non-empty; provider max length to confirm (A1) | parameter_blank |
email | RFC-5322-shaped; optional | parameter_inclusion |
postalcode | FR 5-digit when country=fr; optional | — |
rounding_method | enum (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
| Verb | Path | Purpose |
|---|---|---|
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:
- 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.
- 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_idonto the enrollment. - Confirm safe
PUTfirst (staging-gated). Attempt the lighter-weightPUT /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. - 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:
- A new account (correct-SIREN reprovision, §3.1) has an empty number space — a number previously consumed on the wrong account is free to use again on the new account, because uniqueness is scoped to
{account}. - Green-Got’s own gap-free numbering (invoicing/5. Numbering) is the authoritative sequence; it is allocated per legal entity (SIREN) on the Green-Got side and is not reset by a B2Brouter account recreate. So after a reprovision Green-Got continues its existing sequence and the new B2Brouter account accepts those numbers (they are new to it).
- Reuse safety after recreate: never re-send a number that was already legally transmitted under the prior account — that would be a duplicate legal document, not a fresh issue. The orphaned-invoice rule (§3.1 step 4) governs anything already sent; only never-transmitted numbers are safe to carry into the new account.
- Confirm on staging (8. Staging Verification Matrix A1): that
numberuniqueness is scoped per account (a duplicate within one account returns422 parameter_taken; the same number on a different account is accepted).
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):
- Create with
POST /accounts/{account}/tax_report_settings, sending the fulltax_report_settingbody with the version-pinned profile fields (§4.1). - Update an existing setting with
PUT /accounts/{account}/tax_report_settings/dgfip— also with the full body (refresh stale fields, re-trigger registration).
| Verb | Path | Purpose |
|---|---|---|
POST | /accounts/{account}/tax_report_settings | Create the DGFiP tax-report setting (enables reporting + PPF registration). |
PUT | /accounts/{account}/tax_report_settings/dgfip | Update 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:
| Field | Required | Description |
|---|---|---|
code | Yes | "dgfip". |
start_date | Yes | Date e-reporting begins. |
type_operation | Yes | "services", "goods", or "mixed". |
naf_code | Yes¹ | INSEE activity (NAF/APE) code (2-digit, e.g. "62"). |
enterprise_size | Yes¹ | "micro", "pme", "eti", or "ge". |
reason_vat_exempt | No | VAT-exemption reason; defaults to "VATEX-FR-FRANCHISE". |
email | No | Contact for tax notifications. |
auto_generate | No | Always true for DGFiP (non-modifiable): B2Brouter generates the Flux 1 / Flux 10 reports from each submission. |
auto_send | No | Auto-transmit the generated reports to the authority; defaults to true. |
enabled | No | Activate the setting; defaults to true. |
annuaire_only | No | If 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:
auto_generate=true(fixed for DGFiP) — B2Brouter derives the Flux 1 / Flux 10 reports automatically from each invoice create/send; Green-Got never builds the report payload for the data B2Brouter can derive (4. Sending Invoices §5).auto_send=true(Green-Got’s setting) — generated reports are transmitted to the authority without a further Green-Got call.auto_send=false(the open question): the precise meaning — whether it leaves generated reports in anew/pending state awaiting a Green-Got-initiated submit, and what the manual-submit API is (the report endpoint and verb), and whether a per-transaction / per-invoice override of the account default exists — is not documented and is confirmed on staging (8. Staging Verification Matrix rows C1 / Q3). Green-Got’s MVP keepsauto_send=true(no manual-submit dependency); thefalsepath and any per-invoice override are documented as a staging-gated capability, not asserted as available.
Effect of enabling dgfip:
- B2Brouter registers the SIREN/SIRET in the PPF Annuaire, making the company discoverable for routing. Propagation can take up to 24 hours — the account is not immediately reachable.
- A Peppol
0225transport is created (replacing any existing Peppol transport on the account). - Flux 1 (domestic B2B) and Flux 10 (B2C / cross-border e-reporting) tax reports are generated automatically per the tax-report settings above.
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:
- Invoice creation/send responses return generated report ids in a
tax_report_idsarray. Persist them against the transmission. - Poll a report’s lifecycle via
GET /tax_reports/{TAX_REPORT_ID}. Flux 1 and Flux 10 both movenew → sent → acknowledged → registered(terminal ✅), withrefused/errorand (Flux 1)annulledas failure states. The tax-report status enum is enumerated in 6. Statuses and Webhooks §2.3. - A terminal-state tax-report webhook fires on registration; treat it as a ping and refetch (see 6. Statuses and Webhooks §4).
- Refresh stale DGFiP states: if a setting or report has been sitting in a non-terminal state, re-
PUTthe setting / re-poll the report rather than assuming the last observed state still holds.
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:
- Do not onboard real SIREN/SIRET against DGFiP QAS — it will be rejected.
- Use fictitious Chorus Pro QAS identifiers for end-to-end onboarding tests.
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
- 1. Overview and Concepts — accounts, identifiers, eDocSync.
- 2. Authentication and Environments — auth headers used by these calls.
- 1. Reform Overview — mandate and Annuaire at the reform level.
9. Sources
- B2Brouter — Create account: https://b2brouter.readme.io/reference/create-account
- B2Brouter — DGFiP e-invoicing and e-reporting: https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
- B2Brouter — Get tax report: https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
- B2Brouter — API introduction: https://docs.b2brouter.net/en/developers/start-using-our-api/introduction/
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
- B2Brouter: the certified Plateforme Agréée (PA) Green-Got transmits through. See 2. Platform Architecture.
- Account: a B2Brouter company, keyed to one SIREN. The
{account}path segment identifies it. See 3. Onboarding Accounts. - Contact: a B2Brouter invoice counterparty (client and/or provider), carrying the routing information used to deliver an invoice.
- Transport Type Code: the delivery channel for a contact, e.g.
peppol,fr.chorus,email. Drives routing. - Document Type Code: the document format/profile, which determines the XML B2Brouter generates. The value is channel-specific: B2G / Chorus Pro uses
xml.ubl.invoice.chorus(confirmed in B2Brouter’s Chorus Pro guide); domestic B2B (Flux 1) uses the France CIUS profile, notchorus— the canonical FR B2B document types are the UBL/CIIfrciusprofile (e.g.xml.ubl.invoice.frcius.v1) and Factur-X (PDF/A-3 with embedded CII). The exact B2Bdocument_type_codestring is confirmed on staging (see 8. Staging Verification Matrix D1) —chorusstays scoped to B2G and must not be asserted on the domestic B2B path. See 4. Formats and Invoice Data. - Scheme: a code classification for an identifier (e.g.
0009SIRET, VAT schemes). Note:0225is the PPF/annuaire SIREN routing prefix (see 6. Annuaire and Routing); whether it is also accepted as a B2Broutercin_scheme(alongside the documented0009SIRET) is a wire value confirmed against staging and folded into the mapper — see the FR-scheme design note in 1. Overview and Concepts §4.5. - TIN / CIN: Tax Identification Number / Company Identification Number, each with a scheme + value.
- Directory: B2Brouter’s lookup of a recipient’s reachable transports/document types (distinct from the national Annuaire).
- Flux 1 / Flux 10: domestic B2B e-invoicing flow / e-reporting flow, both performed automatically by B2Brouter once DGFiP is enabled. See 7. E-Reporting.
- Chorus Pro: the legacy B2G portal for invoices to public administrations.
- Base URLs: production
https://api.b2brouter.net, staginghttps://api-staging.b2brouter.net. All calls carryX-B2B-API-Key(see 3. Onboarding Accounts).
2. The Four-Step Outbound Flow
Issuing an invoice through B2Brouter is a four-step sequence:
- Create or look up the recipient contact — establish the counterparty and its routing information.
- Create the invoice —
POSTthe invoice data (Green-Got submits JSON; B2Brouter generates the structured XML). - Send the invoice — trigger transmission to the recipient via the resolved transport.
- 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.
| Verb | Path | Purpose |
|---|---|---|
POST | /accounts/{account}/contacts | Create 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):
| Field | Required | Description |
|---|---|---|
name | Yes | Legal name of the counterparty. |
country | Yes | ISO 3166-1 alpha-2 country code. |
tin_scheme | Yes | Tax identifier scheme (e.g. VAT, TAX). |
tin_value | Yes | Tax identifier value. |
is_client | No | Marks the contact as an invoice recipient (we issue to them). |
is_provider | No | Marks the contact as a supplier (they issue to us). |
transport_type_code | No | Delivery channel: peppol, fr.chorus, email, etc. Drives routing. |
document_type_code | No | Output 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_scheme | No | Company identifier scheme (e.g. 0009 SIRET, DIR3). |
cin_value | No | Company identifier value. |
language | No | Correspondence language. |
terms | No | Default payment terms text. |
public_sector | No | Flags a public-administration recipient (B2G). |
payment_method | No | Default 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):
| Outcome | HTTP | Meaning | Green-Got handling |
|---|---|---|---|
Found | 200 | Company resolved; supported transports / document types returned. | Terminal. Use to populate transport_type_code / document_type_code. Cacheable. |
Processing(polling_url) | 202 | Participant 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). |
NotRegistered | 404 | The participant is not registered in the Peppol network. | Terminal. Recipient genuinely absent (see fail-closed routing rules in §3). |
RegisteredButIncomplete(code) | 424 | Registered in Peppol, but the participant’s SMP did not provide the data B2Brouter depends on to register it as an exchange partner. code ∈ no_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:
- Never cache a
202. A still-syncing participant is transient, not absent — caching it would persist a wrong result. Retry the returned polling URL until it reaches a terminal outcome. - Do not treat a still-syncing (
202) participant asNotFound. Conflating202with404would make a designation/routing sweep falsely flag a reachable recipient as lost routing. The transient-vs-terminal boundary here mirrors the no-route propagation rule in §3 (a not-yet-propagated recipient stays in the durable retry loop, not surfaced as terminal failure). - Map
424to operator remediation.RegisteredButIncompleteis distinct from bothFoundandNotRegistered: the recipient exists in Peppol but is not exchange-ready — surface it for operator action (the cause is the recipient’s SMP), and do not record it as a successful resolution.
2.2 Step 2 — Create the invoice
| Verb | Path | Purpose |
|---|---|---|
POST | /accounts/{account}/invoices | Create an issued invoice. Returns 201 with id. |
Fields on POST /accounts/{account}/invoices (in the invoice object):
| Field | Required | Description |
|---|---|---|
type | Yes | IssuedInvoice, IssuedSelfInvoice, or IssuedSimplifiedInvoice. |
contact_id | Yes | The recipient contact’s id from step 1. |
number | Yes | Invoice number, unique per account. |
date | Yes | Issue date. |
currency | Yes | ISO 4217 currency code. |
due_date | Yes | Payment due date. |
invoice_lines_attributes[] | Yes | Line items (see below). |
type_document | No | UNCL1001 document type: 380 commercial invoice, 381 credit note (avoir), 386 prepayment, 389 self-billed. |
buyer_reference | No | Buyer routing/service reference (required for Chorus Pro — see §3). |
remittance_information | Yes¹ | Remittance reference carrying the PMD payment data (structured/unstructured remittance reference). Required France B2B native field. |
payment_method_text | Yes¹ | The PMT payment-method text (how the invoice is to be paid). Required France B2B native field. |
payment_terms | Yes¹ | The AAB payment terms — due date, penalties, discount conditions. Required France B2B native field. |
iban | No | Creditor IBAN (metadata only; B2Brouter does no settlement). |
order_number | No | Purchase order reference. |
terms | No² | 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 param | Required | Description |
|---|---|---|
invoice | Yes | The invoice object (fields above). |
send_after_import | No | Boolean. true queues an immediate async send after create. |
ack | No | Boolean acknowledgement flag. |
Each entry of invoice_lines_attributes[]:
| Field | Required | Description |
|---|---|---|
quantity | Yes | Line quantity. |
price | Yes | Unit price. |
description | Yes | Line description. |
taxes_attributes[] | Yes | One or more taxes, each { name, category, percent }. |
Each entry of taxes_attributes[]:
| Field | Description |
|---|---|
name | Tax label (e.g. VAT). |
category | EN 16931 VAT category code (UNCL5305: S, Z, E, AE, K, G, O). |
percent | Rate 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
| Verb | Path | Purpose |
|---|---|---|
POST | /invoices/send_invoice/{id} | Transmit the invoice via the resolved transport. No request body. |
Responses:
204— accepted for sending.422— cannot send (validation failure or no resolvable route).
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):
- If draft deletion is supported: discard the
newinvoice before send; the gap-free number it consumed is handled by Green-Got’s numbering reconciliation (invoicing/5. Numbering) — a deleted-pre-send number is a recorded, reconciled gap, not a silent hole. - If draft deletion is NOT supported: the only safe correction once a number is bound is the standard post-issuance path — issue a credit note (avoir,
type_document381) and re-issue. Never mutate a sent invoice in place.
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
| Verb | Path | Purpose |
|---|---|---|
GET | /invoices/{id}?include=detailed_lines | Retrieve 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:
- Legal invoice file — the final official file delivered (for an issued invoice). This is what Green-Got archives.
- Original invoice file — the first file B2Brouter received for the invoice (relevant on the received side; see 5. Receiving Invoices §5).
- Generated export — a format conversion B2Brouter can produce on demand; not the legal artefact.
attachments[]— additional/supporting files linked to the invoice. Attachments are NOT the invoice itself and must not be archived as the legal artefact.
Canonical retrieval for an ISSUED invoice — fetch the legal file:
| Verb | Path / field | Purpose |
|---|---|---|
GET | download_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/legal | The 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:
- If the contact carries a
transport_type_code, that transport is used. - Otherwise B2Brouter performs a directory lookup to discover a reachable transport.
- 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 24h — 3. 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:
- Inside the propagation window the transmission stays in the durable retry loop (7. API Mechanics §10.3.1) with backoff — it is not surfaced as a terminal
422. - Past the propagation window (or on a
422that the staging capture shows is a genuine unreachable-recipient rejection, not propagation latency), it becomes a terminal routing failure and follows the recipient-not-found path (a domestic B2B recipient genuinely absent from the Annuaire is a rejection / Rejetée, never a Flux 10 fallback — see 6. Annuaire and Routing). - Confirm on staging (8. Staging Verification Matrix D1 / A4): the directory-lookup and send responses for a recipient that is registered-but-not-yet-propagated vs genuinely-unreachable, so the transient-vs-terminal boundary is driven by the captured error shape, not a guess. Until captured, treat directory no-route within 24h of a known recipient enablement as transient.
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):
trueornil/absent — permissive: the invoice generates a Flux 1 tax report and proceeds (the asynchronous registration/transmission lifecycle runs normally;nilis the unverified-but-proceed case).false— the contact is NOT registered in the Annuaire, so B2Brouter creates the invoice object but generates NO Flux 1 tax report, deliberately preventing an invalid PPF submission. A2xx/201here is therefore NOT evidence of legal transmission for a domestic FR B2B operation.
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:
| Field | Value |
|---|---|
transport_type_code | fr.chorus |
document_type_code | xml.ubl.invoice.chorus |
cin_scheme | 0009 (SIRET) |
cin_value | the recipient’s SIRET |
public_sector | true |
Invoice requirements for Chorus Pro:
ponumber— the purchase-order reference required by the administration.buyer_reference— the Chorus Pro service code identifying the receiving department.
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:
- Flux 1 (domestic B2B e-invoicing): B2Brouter generates the structured document (UBL/CII/Factur-X) from the JSON and routes it to the recipient’s PA via the PPF, reporting invoice data and lifecycle statuses to the PPF concentrator.
- Flux 10 (e-reporting): for B2C and cross-border transactions, B2Brouter transmits e-reporting data to DGFiP via the PPF concentrator.
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):
in_dgfip_annuaire: false→ the invoice object exists but no Flux 1 tax report is generated (the contact is not Annuaire-registered, §3).- A non-empty
errors[]on the response → the invoice is created (HTTP200/201) but never transmitted to the PPF (e.g. a category-Eline missing its requiredcomment). The guide is explicit: “the invoice is created but never transmitted to the PPF; the response will contain a non-emptyerrorsarray — check it even on successful responses.”
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:
errors[]empty or non-blocking — any blocking error inerrors[]means not transmitted, even on a2xx.- Expected
tax_report_idspresent — the response/refetch carries the tax-report id(s) the flow requires (tracked and polled per 3. Onboarding Accounts §4.2). - 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. - Annuaire / tax-report linkage — for domestic FR B2B,
in_dgfip_annuaireis notfalseand 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):
- If a download endpoint exists, Green-Got fetches and stores the report/ledger as supplementary audit evidence alongside the tax-report id and lifecycle state — useful for reconciliation and audit corroboration.
- This is not required to meet the retention obligation. Retention is already satisfied by the underlying data Green-Got owns (the issued legal artefact archived per 8. Archiving and Audit, the append-only allocation ledger, and the persisted
tax_report_ids+ statuses + hashes). A missing report-download endpoint does not breach retention — do not frame the absence of a Flux 10 ledger download as a retention violation; the durable evidence is the underlying data + ids + hashes.
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”:
-
JSON-create path (default):
POST /accounts/{account}/invoiceswith the invoice as JSON; B2Brouter generates the correct format (UBL / CII / Factur-X) per recipient; retrieve viaGET. This is the path described in §2. -
XML-import path (Green-Got-generated XML): when Green-Got supplies a pre-built structured document, it is imported, not created-and-attached:
Verb Path Purpose POST/accounts/{account}/invoices/import?send_after_import=falseImport a pre-built invoice document and leave it in newfor review.- The request body is the invoice file as raw bytes (the default — not base64-encoded), and the import endpoint’s request media type is
Content-Type: application/octet-stream(a binary invoice file). This is what the B2Brouter import OpenAPI declares; a wrong media type returns406 Not Acceptable(alongside401/403), so the byte stream MUST be sent asapplication/octet-stream, not a format-specific type. Format-specific media types (application/pdffor Factur-X PDF/A-3 with embedded CII,application/xmlfor raw UBL 2.1 / CII) are only used on this endpoint if a staging capture (8. Staging Verification Matrix D1) proves the import endpoint accepts them — until then the canonical import body isapplication/octet-stream. (Note: the separate/documents/validateendpoint legitimately accepts the typed bodies —application/pdf,application/xml, plusapplication/octet-streamandtext/plain— but those media-type rules apply to/documents/validate, not to…/invoices/import.) send_after_import=falseimports without transmitting (review first, thenPOST /invoices/send_invoice/{id}— §2.3).send_after_import=trueimports and queues an immediate async send.
- The request body is the invoice file as raw bytes (the default — not base64-encoded), and the import endpoint’s request media type is
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
- 2. Platform Architecture — five-corner model, Flux 1 vs Flux 10.
- 4. Formats and Invoice Data — Factur-X/UBL/CII, EN 16931 fields, JSON-to-XML mapping.
- 5. Lifecycle Statuses — AFNOR statuses surfaced from the send.
- 3. Onboarding Accounts — account creation and DGFiP enablement.
- 6. Statuses and Webhooks — status transitions after send.
- 7. API Mechanics — error codes, concurrency, retries.
9. Sources
- Send invoices end to end: https://developer.b2brouter.net/docs/send_invoices_end_to_end
- Create invoice (top-level
send_after_import/ackalongsideinvoice): https://developer.b2brouter.net/reference/create-invoice - Import invoice (request media type
application/octet-stream, binary invoice file;406 Not Acceptableon a wrong media type, alongside401/403): https://developer.b2brouter.net/reference/import-invoice - Download legal or original invoice (issued →
download_legal_url; attachments are supplemental): https://docs.b2brouter.net/en/developers/essential-guides/download-invoice - Get invoice as document type (
/invoices/{id}/as/{document_type}, incl.legal/original): https://developer.b2brouter.net/reference/get-invoice-with-document-type - Create contact: https://developer.b2brouter.net/reference/create-contact
- Send invoice: https://developer.b2brouter.net/reference/send-invoice
- Send an invoice through Chorus Pro: https://developer.b2brouter.net/docs/send_an_invoice_through_chorus_pro
- Directory: https://developer.b2brouter.net/docs/directory
- Directory lookup by country and scheme (
200Found /202Accepted with polling — “retry the request” /404not in Peppol /424Failed Dependencyno_businesscardincomplete_businesscardno_exportable_doctypes): https://developer.b2brouter.net/reference/lookup-directory-by-country-and-scheme - Send an invoice through Peppol: https://developer.b2brouter.net/docs/send_an_invoice_through_peppol
- DGFiP / Flux integration: https://developer.b2brouter.net/docs/dgfip
- DGFiP e-invoicing & e-reporting guide (
in_dgfip_annuairefalse→ no Flux 1 report; non-emptyerrors[]on a created-but-not-transmitted invoice — “check it even on successful responses”; Flux 1 vs Flux 10 tax reports): https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/ - Document type codes: https://developer.b2brouter.net/docs/peppol_cii_type_document_codes
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
- ReceivedInvoice: the B2Brouter invoice type for an inbound document (a supplier’s invoice to us).
- Reception channel: how the document reached the account — Peppol, email, the B2Brouter network, or manual upload.
issued=false: marks a manually uploaded inbound document (as opposed to one received over a transport).- Reception status: B2Brouter’s inbound state:
new,received, orinvalid(see §4). Distinct from the issued-invoice statuses in 6. Statuses and Webhooks. - Factur-X / ZUGFeRD: a hybrid PDF/A-3 carrying an embedded CII XML. B2Brouter extracts the structured XML into JSON.
contact(on a received invoice): the issuer (the supplier), not the recipient.- AP (accounts payable): supplier invoices Green-Got owes. Modelled in the
bills/crate, not ininvoicing. - Base URLs: production
https://api.b2brouter.net, staginghttps://api-staging.b2brouter.net. All calls carryX-B2B-API-Key. - API version: requests carry
X-B2B-API-Version(current default2026-04-20; DGFiP-specific endpoints/fields require a minimum of2026-03-02). Pin this header explicitly so the inbound contract is stable.
2. Reception Channels
Inbound invoices arrive automatically once the account’s transports are enabled (see 3. Onboarding Accounts). Channels:
| Channel | How it arrives |
|---|---|
| Peppol | Delivered over the Peppol network to B2Brouter’s Access Point for the account. |
| A document emailed to the account’s reception address. | |
| B2Brouter network | Delivered internally when the sender is also on B2Brouter (P2P). |
| Manual upload | Imported 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
| Verb | Path | Purpose |
|---|---|---|
GET | /accounts/{account}/invoices?type=ReceivedInvoice | List inbound invoices for the account. |
Query parameters:
| Param | Default | Description |
|---|---|---|
type | — | Set to ReceivedInvoice to scope to inbound. |
offset | 0 | Pagination offset. |
limit | 25 | Page size, max 500. |
| status filters | — | new, received, invalid (and downstream accepted, refused, annotated; paid is [open — provider-support] for the French received flow — see §4). |
| date filters | — | Filter by date range. |
| identifier filters | — | tin_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
| Status | Meaning |
|---|---|
new | Manually imported; not yet processed through a transport. |
received | Delivered via a transport (Peppol, email, network). |
invalid | Failed 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 tiesallegedly_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 ownbills/(AP) record, not by a B2Brouter receivedmark_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_as — 6. Statuses and Webhooks §3). The transitions Green-Got relies on for the French received flow:
| From | To | Trigger | FR legal target | Notifies sender? |
|---|---|---|---|---|
| (none) | received | Delivered via a transport (Peppol / network / email) | — (arrival) | — |
| (none) | new | Manually imported (issued=false) | — (arrival) | — |
received / new | invalid | B2Brouter validation failure (syntax/business) | — | — |
received / new | accepted | mark_as accepted (buyer accepts) | CDAR 205 Approuvée | commit=with_mail only |
received / new | refused | mark_as refused (buyer refuses) | CDAR 210 Refusée | commit=with_mail only |
received / new / accepted | annotated | mark_as annotated (internal note) | — (internal only) | No |
accepted / refused | paid | mark_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
| Verb | Path | Purpose |
|---|---|---|
GET | /invoices/{id}?include=lines | Fetch one received invoice with parsed line data. |
Useful parameters:
| Param | Description |
|---|---|
include=lines | Include parsed line items (detailed_lines for the fuller form). |
disposition=inline | Render the original PDF inline rather than as a download. |
ack=true | Include acknowledged invoices in scope. |
Response contents:
- The full invoice as structured JSON (header, lines, taxes, totals).
contact— the issuer (supplier) of the received invoice.attachments[]— supplemental files linked to the invoice (each with a download URL). These are NOT the original invoice itself and must not be archived as the legal/original artefact.
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:
| Verb | Path | Purpose |
|---|---|---|
GET | /invoices/{id}/as/original | Returns 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:
- 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.
- 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
- 4. Sending Invoices — the outbound counterpart.
- 6. Statuses and Webhooks — reception status transitions and event delivery.
- 7. API Mechanics — pagination, attachment limits, errors.
- 10. Integration Contracts — the eventbus contract handing inbound to
bills/. - 4. Formats and Invoice Data — Factur-X/CII structure.
9. Sources
- Receive, integrate and manage received invoices: https://developer.b2brouter.net/docs/receive_integrate_and_manage_received_invoices
- Get invoices (list): https://developer.b2brouter.net/reference/get-invoices
- Get invoice (details): https://developer.b2brouter.net/reference/get-invoice
- Download legal or original invoice (originals via
/as/original; attachments are supplemental): https://docs.b2brouter.net/en/developers/essential-guides/download-invoice - Get invoice as document type (
/invoices/{id}/as/{document_type}, incl.original/legal): https://developer.b2brouter.net/reference/get-invoice-with-document-type - DGFiP e-invoicing and e-reporting (France) — received
mark_astargets, CDAR codes, API version: https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/ - Mark as (states by invoice type): https://b2brouter.readme.io/reference/mark-as-invoice
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
- Issued status: the lifecycle of an invoice Green-Got sends.
- Received status: the lifecycle of an inbound supplier invoice.
- Tax-report status: the lifecycle of the data B2Brouter reports to DGFiP (e-reporting, Flux 10).
mark_as: the endpoint that moves an invoice to a business state. The generic enum isaccepted | refused | paid(plus type-specific values, see §3); for French RECEIVED invoices onlyaccepted/refusedare confirmed legal targets.ack: the acknowledge endpoint; hides an invoice from default listings.- Webhook: a B2Brouter-originated HTTP
POSTto a Green-Got endpoint on a state change. Managed via the/web_hooksAPI (create/list/update/delete — see §4), not UI-only. - Signature header:
X-B2Brouter-Signature: t=<timestamp>,s=<signature>— an HMAC-SHA256 used to authenticate webhook payloads. - Events: a pollable record of state changes, used as a fallback to webhooks.
- CDAR (Compte-Rendu d’Acceptation/Rejet): PPF delivery/acceptance receipts that flow back onto the invoice object.
- Base URLs: production
https://api.b2brouter.net, staginghttps://api-staging.b2brouter.net. - API version: requests carry
X-B2B-API-Version(current default2026-04-20; DGFiP-specific endpoints/fields require a minimum of2026-03-02). Pin this header explicitly.
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
| Status | Meaning |
|---|---|
new | Created, not yet sent. |
sending | Transmission in progress. |
sent | Transmitted to the recipient channel. |
accepted | Accepted by the buyer. |
registered | Tax-reported (acknowledged by the authority). |
refused | Refused by the buyer. |
paid | Marked paid. |
closed | Terminal/closed. |
error | Transmission or processing error. |
downloaded | Retrieved by the recipient. |
2.2 Received invoice statuses
| Status | Meaning |
|---|---|
new | Manually imported. |
received | Delivered via a transport. |
invalid | Failed validation. |
accepted | Accepted (set via mark_as). Confirmed French received target (CDAR 205 Approuvée). |
refused | Refused (set via mark_as). Confirmed French received target (CDAR 210 Refusée). |
paid | Marked 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). |
annotated | Internal 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:
- Flux 1 (domestic B2B):
new,sent,acknowledged,registered,refused,error,annulled. - Flux 10 (e-reporting):
new,sent,acknowledged,registered,refused,error.
| Status | Meaning | Terminal? | Retryable? | Flux 1 | Flux 10 |
|---|---|---|---|---|---|
new | Report queued (accumulated for the batch). | No | — (in progress) | ✅ | ✅ |
sent | Deposited to the authority (PPF via SFTP). | No | — (in progress) | ✅ | ✅ |
acknowledged | Received / validated by the authority (PPF). | No | — (in progress) | ✅ | ✅ |
registered | Accepted by DGFiP (CDV condition 300). | Yes | No (success) | ✅ | ✅ |
refused | Rejected by DGFiP (CDV condition 301). | Yes | Yes (fix and resubmit) | ✅ | ✅ |
error | Transmission or processing error. | Yes | Yes (transient — retry) | ✅ | ✅ |
annulled | Invoice annulled after registration. | Yes | No | ✅ | — |
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).
| Verb | Path | Purpose |
|---|---|---|
GET | /invoice_states | List the available invoice states. |
3. State-Transition Endpoints
| Verb | Path | Purpose |
|---|---|---|
POST | /invoices/{id}/mark_as | Switch an invoice to a business state. |
POST | /invoices/{id}/ack | Acknowledge an invoice (hides it from default listings unless ack=true). |
Body of POST /invoices/{id}/mark_as:
| Field | Description |
|---|---|
state | Target state. Valid values are type-specific — see below. |
reason | Free-text reason (e.g. why refused); included in the email when commit=with_mail. |
commit | with_mail emails the contact on the transition. |
Valid state values by invoice type (provider enum, API 2026-04-20):
| Invoice type | Accepted state values |
|---|---|
| Issued | new, sent, accepted, registered, refused, paid, closed |
| Simplified issued | new, sent, paid, downloaded, accepted, refused, registered, closed |
| Self-invoice | paid, accepted, registered, closed |
| Received | new, 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
| Verb | Path | Purpose |
|---|---|---|
POST | /web_hooks | Create 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_hooks | List 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:
- Store
signing_secretimmediately on create (straight into the secret store, §4.5) — it cannot be retrieved again later. - Rotate by creating a REPLACEMENT webhook (which yields a fresh
signing_secret) and then disabling/deleting the old one — not by updating the existing webhook.PATCH /web_hooks/{id}changes URL/enabled/events/description only.
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:
- Read the
X-B2Brouter-Signatureheader and split it intot=<timestamp>ands=<signature>. - Capture the raw request body bytes exactly as received — do not re-serialize parsed JSON.
- Construct the signed payload string
signed = timestamp + "." + raw_body. - Compute
expected = HMAC_SHA256(signing_key, signed)and apply the same digest encoding B2Brouter uses (assumed hex — see note above). - Compare
expectedagainstsignatureusing a constant-time comparison. - Reject the request (do not process) if the comparison fails, and emit an alert on every signature failure (§4.2.1).
- Reject if
timestampis outside the acceptable freshness window (replay protection is mandatory — §4.2.1). - 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:
- Maximum clock skew / freshness window. Reject any webhook whose
t=<timestamp>is older than a bounded window (e.g. ±5 min) or set in the future beyond a small skew allowance. Old and future-dated timestamps are rejected outright. - Processed-event / signature-id store. Persist a stable identity per delivery — the provider event id if present, else the signature
s, else a hash of(timestamp, raw_body). Reject (idempotently ack-and-drop) any delivery whose identity is already recorded. This bounds replay even within the freshness window. Replay protection does not depend on the event-id field existing: whether the published payload actually carries a stable per-delivery event id is confirmed on staging (8. Staging Verification Matrix row E3, Uncertainties W-2); the fallback chain (signatures→ hash of(timestamp, raw_body)) gives a stable identity even if no event id is present, so dedupe is correct either way. - Raw-body hash. Record a hash of the exact raw body bytes alongside the event id, both for dedupe and for tamper-evidence/traceability.
- Alert on signature failures. Any signature-verification failure is a security signal: count it, alert on a threshold, and log enough context (no secrets) to investigate. A spike indicates key compromise, misconfiguration, or an attack.
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:
| Field | Use | Confirmed? |
|---|---|---|
| event id | Stable 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 id | The resource whose state changed — the key to refetch. | Documented |
| state | The new provider state (one of the §2 enum values; tolerate unrecognised values per §2). | Documented |
| notes | Provider-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):
- Storage location. Secrets live in the managed secret store / KMS (never in source, env files committed to git, or logs). The webhook
signing_secretis returned only on create through the/web_hooksAPI (§4.0) and the value must be written straight to the secret store at that moment — it cannot be re-fetched afterwards. - IAM ownership. A named owning role/team holds change rights; access is least-privilege and granted per-environment (staging vs production keys are distinct and never shared).
- Rotation cadence. Rotate on a fixed schedule (e.g. quarterly) and on personnel change. Rotation is a create-replacement, not an update:
POST /web_hooksto create a new subscription (capturing its freshsigning_secret), run a brief dual-accept window so in-flight deliveries signed with the old secret still verify, then disable/DELETE /web_hooks/{id}the old webhook. There is no secret-regeneration onPATCH /web_hooks/{id}(update changes URL/enabled/events/description only) unless B2Brouter documents one. - Emergency revocation. On suspected compromise, immediately
DELETE /web_hooks/{id}to stop deliveries and create a replacement webhook (newsigning_secret), then re-provision. Revocation is a documented, runnable procedure, not ad-hoc. - Redaction rules. Signing keys, API keys, and full signatures are redacted from logs, traces, and error reports. Log only non-secret identifiers (event id, invoice id, truncated/hashed signature) needed for traceability.
- Audit logs. Every read or change of a webhook secret (create, retrieve, rotate, delete) is recorded in an audit log with actor, time, and environment — supporting incident review and compliance.
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:
- Storage. API keys live in the managed secret store / KMS, loaded per environment via CurrentEnvParameters (2. Authentication and Environments §5); never in source, committed env files, or logs. Keys are environment-scoped — a
test_-prefixed sandbox key, a staging key, and a production key are distinct and never shared (using the wrong key on the shared sandbox/prod host silently routes to the wrong zone — 2. Authentication and Environments §3). - Rotation cadence. Rotate on a fixed schedule (e.g. quarterly) and on personnel change. B2Brouter has no documented self-service key-regeneration API, so rotation is operator/support-mediated: obtain a new key, then perform a zero-downtime dual-key swap — load the new key alongside the old (the client accepts either), cut new requests over to the new key, drain in-flight requests, then retire the old key. Because B2Brouter accepts the same key on every call (no per-request key binding), the swap is a config rollout, not a code change.
- Emergency revocation. On suspected compromise, request immediate revocation of the affected key via B2Brouter support and roll the replacement key through the same dual-key swap. Revocation is a documented, runnable procedure, not ad-hoc. (Whether a self-service revocation/rotation endpoint exists is a confirm-on-staging / support item — 8. Staging Verification Matrix.)
- Redaction. The
X-B2B-API-Keyis redacted from logs, traces, and error reports exactly like the webhook signing key (§4.5). Never persist it in any durable store; log only non-secret identifiers. - Audit. Every storage, rotation, or revocation of an API key is recorded in the ComplianceEventLog (10. Integration Contracts §12) with actor, time, and environment, so a credential change is reconstructable for incident review and compliance.
5. Events Polling Fallback
When a webhook is missed (endpoint downtime, misconfiguration), state changes can be recovered by polling.
| Verb | Path | Purpose |
|---|---|---|
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 AFNOR XP Z12-012 legal lifecycle (4 mandatory: Déposée, Rejetée, Refusée, Encaissée; plus recommended), and
- Green-Got’s internal status.
The authoritative three-layer mapping table lives in 5. Lifecycle Statuses; this document only enumerates the B2Brouter layer.
8. Related Documents
- 4. Sending Invoices — produces the issued statuses.
- 5. Receiving Invoices — produces the received statuses; polling-authoritative inbound (webhooks are a hint).
- 5. Lifecycle Statuses — AFNOR ↔ B2Brouter ↔ internal mapping.
- 7. API Mechanics — quota, error codes, idempotency stance.
9. Sources
- Get invoice states: https://developer.b2brouter.net/reference/get-invoice-states
- Mark as (states by invoice type, API
2026-04-20): https://b2brouter.readme.io/reference/mark-as-invoice - Acknowledge: https://developer.b2brouter.net/reference/ack-invoice
- Webhooks guide (signature verification): https://docs.b2brouter.net/en/developers/essential-guides/webhooks/
- Create web hook (
/web_hooksAPI): https://developer.b2brouter.net/reference/create-web-hook - DGFiP e-invoicing and e-reporting (France) — CDAR codes, received
mark_astargets: https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
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
- Concurrency: the number of simultaneous in-flight requests on one API key. B2Brouter publishes no concurrency limit; the figure used here is a Green-Got self-imposed throttle (§2).
- Rate limit: the request count permitted per minute — B2Brouter’s official, published limit.
X-B2B-API-Request-Id: a per-response identifier B2Brouter returns on every logged response (not only errors), used for support correlation. Error responses additionally carry arequest_log_urlfield pointing at the request log.- Query language: the operator syntax accepted on
GET /accountsfilters. - Validation: B2Brouter’s XSD (syntax) + Schematron (business rules) check of a document. B2Brouter exposes standalone validation (
POST /documents/validate,GET /invoices/{id}/validate) and runs the same checks inline at invoice create/import — the create/import gate is the final authoritative check before send (§8). - Base URLs: production
https://api.b2brouter.net, staginghttps://api-staging.b2brouter.net. All calls carryX-B2B-API-Key(see 3. Onboarding Accounts).
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:
| Aspect | Production | Staging / Sandbox | Provenance |
|---|---|---|---|
| Rate limit (per API key) | 1,000 req/min | 600 req/min | Official, 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 class | Default request timeout | Rationale |
|---|---|---|
Standard calls (GET, contact/account create, mark_as, ack, status/list polls) | 30s | Ordinary REST round-trips. |
Generation-heavy calls (POST …/invoices, …/invoices/import, send_invoice, /documents/validate) | 120s | B2Brouter 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.
| HTTP | Codes | Meaning |
|---|---|---|
400 | invalid_request_params, invalid_api_version, missing_api_version, api_version_subdomain_mismatch | Malformed request parameters, or an unsupported / missing / mismatched API version (see 2. Authentication and Environments). |
401 | unauthorized, missing_api_key, invalid_api_key, expired_api_key | Authentication failure (including a key used in the wrong environment). |
403 | no_account_group | Authorized but not permitted (no group / permissions linked to the key). |
404 | resource_missing | The target resource does not exist or is inaccessible. |
409 | conflict | Current resource state rejects the operation. |
422 | parameter_blank, parameter_empty, parameter_inclusion, parameter_taken, parameter_cannot_change | Validation/business-rule failure (e.g. duplicate number → parameter_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 usedcannot_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 vault — never standard-log it and never commit it to evidence/PR artifacts. See 4. Sending Invoices.
3.1 Retry guidance
- Retry transient failures (network errors,
429/5xxif returned) with exponential backoff, respecting the concurrency cap. - Do not blindly retry
422/409— these are deterministic and a retry repeats the same failure (and, for sends, risks duplication; see §5). - The B2Brouter PHP SDK retries network failures with backoff; mirror that behavior.
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.
| Param | Default | Description |
|---|---|---|
offset | 0 | Starting record. |
limit | 25 | Page 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:
- Key each operation on a stable Green-Got id — the invoice / transmission id + the operation (e.g.
create,send). Persist the outcome (B2Brouter invoice id, status, request id) against that key. - Never double-submit on retry. Before (re-)issuing an operation, check the persisted outcome for its key; if it already succeeded, return the stored result instead of calling B2Brouter again. Combine with
numberuniqueness per account (a duplicatecreatereturns422 taken) and, on send, never retry a send that may have succeeded without first refetching the invoice status (GET /invoices/{id}). - Account creation dedupes on normalized-SIREN uniqueness (a Green-Got-owned SIREN uniqueness key) plus a pre-create lookup on the company identity —
GET /accounts?cin_value=<siren>(scheme0002), nevertin_value=<siren>(tin_valuecarries the VAT number, not the SIREN). Look up first, create only if absent, and treat a concurrent422 takenas “already exists” (re-fetch). See 3. Onboarding Accounts.
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:
- Dedupe by a stable event / invoice id + status (transition key); persist processed event ids and ignore replays.
- A webhook is a ping, not the truth. It signals “something changed”; the authoritative state is refetched via
GET /invoices/{id}(and persisted raw API status + CDAR, see 5. Lifecycle Statuses). Handlers must be idempotent — reprocessing the same event is a no-op. - See 6. Statuses and Webhooks for the transition-key shape and signature verification.
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:
| Operator | Meaning |
|---|---|
= | Equals. |
~ | Contains / matches. |
> < >= <= | Comparison. |
- | Negation. |
AND / OR | Boolean combination. |
Queryable fields: tin_value, tin_scheme, cin_value, cin_scheme, country, name, identifier, created_at, status.
7. Attachments
| Constraint | Limit |
|---|---|
| Max file size (general) | ≤ 50 MB per file. |
| Max total size (email transport) | ≤ 40 MB total. |
| Allowed types | PDF, XML, office documents, CSV. |
| Blocked | Executables. |
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):
| Verb | Path | Validates |
|---|---|---|
POST | /documents/validate | Side-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}/validate | Validates 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):
| Verb | Path | Validates |
|---|---|---|
POST | /accounts/{account_id}/invoices | JSON-create path — the posted invoice object is validated; failures return 422 and no invoice is created. |
POST | /accounts/{account_id}/invoices/import | XML / 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:
- Optional standalone pre-flight: Green-Got may call
POST /documents/validate(side-effect-free) on a generated document, orGET /invoices/{id}/validateon a stored one, to surface defects early without creating/persisting anything. - At import/create (authoritative): the
POST …/invoicesorPOST …/invoices/importcall is the authoritative validation gate. Green-Got drives this withoutsend_after_importwhen it wants to separate “validated and created” from “transmitted”, or withsend_after_import: trueto validate-and-send atomically (§10). A document is never transmitted on a validation failure. - Local pre-checks (advisory only): Green-Got may also run its own structural pre-checks to fail fast on obvious omissions, but these are advisory and never a substitute for the provider’s XSD/Schematron validation.
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
- Parallelize, but cap concurrency at ~4 per key to respect B2Brouter’s limits; spread bulk work under its per-minute ceiling (§2).
- Retry with backoff only for transient/network failures; never auto-retry deterministic
4xx. - Always capture
X-B2B-API-Request-Idon failures for support. - Own idempotency on both directions (§5): outbound operations keyed on a stable Green-Got id with persisted outcomes; inbound webhooks deduped and treated as a ping that triggers an authoritative refetch. B2Brouter provides no idempotency key.
- Validation (§8): standalone validation exists (
POST /documents/validate,GET /invoices/{id}/validate) for optional pre-flight, but the create/import422is the authoritative gate. Store per the field-level retention rule (§3.2, 13. Privacy §4.1): permanently persist only the normalized validation error codes, theX-B2B-API-Request-Id, and a hash of the422body. The raw422body goes to the short-TTL encrypted vault only if needed and non-secret; therequest_log_urlis a signed bearer URL and is never persisted in any durable store.
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:
- Decoupling — the customer’s send action returns as soon as the transmission is durably accepted into the queue; it never blocks on B2Brouter being reachable, fast, or under its rate cap.
- Durability — an accepted send is a persisted queue entry that survives process restarts and deploys. Nothing in flight is lost if a worker dies mid-attempt.
- Retry — transient failures (rate-limit,
5xx, network) are retried automatically with backoff, and the loop stops on exactly one of success, a terminal business error, a deadline-margin escalation, or retry-budget exhaustion → dead-letter (§10.3.1); no human re-drives a send by hand.
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:
- Concurrency — at most ~4 in-flight requests per API key (the point beyond which B2Brouter latency degrades). The dispatcher holds this as a hard cap.
- Per-minute throughput — stays under the per-minute ceiling (~1,000/min prod, ~600/min staging) with margin. At expected volumes this is never binding, but the cap is enforced regardless.
- Server-driven backoff — if B2Brouter returns a rate-limit signal (
429and/or aRetry-After/X-RateLimit-*header), the worker honors it: it backs off for the indicated delay rather than its own computed backoff. Backoff is still bounded by the §10.3.1 stop conditions (it does not retry forever).
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:
- 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.
- 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.
- Attempt send via B2Brouter — execute the create +
send_invoicesequence (4. Sending Invoices §2) as Temporal activities, capturingX-B2B-API-Request-Idon every failure (§3). - 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-suppliedRetry-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). - Stop on terminal/business failure — a terminal error (validation
422, routing-failure422, 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. - 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:
- Success — the operation completes; the workflow settles (step 6).
- 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. - 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.
- 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 outcome | B2Brouter signal | Lifecycle result (5. Lifecycle Statuses) |
|---|---|---|
| Accepted into queue | — (internal) | Transmission durably pending; not yet a legal status. |
| Send accepted | 204 on send_invoice | Progresses toward Déposée (200) and the outbound flow (§5). |
| Terminal validation/routing failure | 422 | Surfaced as a canonical internal error (§1.1); on a platform rejection, Rejetée (213). |
| Buyer business refusal (later, via webhook) | inbound status | Refusé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.
| Class | Triggers | Worker behavior |
|---|---|---|
| Transient | Network errors, client-side timeout (§2.1), 429 (with Retry-After if present), 5xx | Retry 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 / business | 422 validation (bad field), 422 routing (no reachable recipient — 4. Sending Invoices §3), 409 conflict, buyer Refusée, platform Rejetée | Stop. 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):
- On timeout, first refetch the resource state —
GET /invoices/{id}(orGET /accounts?cin_value=…for a create, §5.1) — to learn whether the prior attempt actually took effect. - If the refetch shows the operation succeeded, settle on that outcome (no re-send); the idempotency key (§5) records it.
- If the refetch shows it did not take effect, retry the operation with backoff.
- 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
| SLO | Target | Alert trigger |
|---|---|---|
| Transmission settles (enqueue → terminal success/business-failure) | 99% within 1h, 99.9% within 24h | A transmission stays non-terminal beyond the threshold. |
Send-attempt success rate (excluding deterministic business 422/refusals) | ≥ 99% over rolling 1h | Transient-error rate breaches threshold (signals a B2Brouter or routing outage). |
| Webhook → authoritative refetch latency | p95 < 5 min | Backlog of unprocessed events grows. |
| Received-invoice poll freshness | Last successful poll < 2× poll interval | Polling loop stalled. |
429 budget headroom | < 50% of per-minute ceiling sustained | Approaching 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):
- The Temporal workflow records the last error against the transmission (permanent: request id + normalized error code + raw-body hash; the raw body /
request_log_urlonly in the short-TTL support vault per §3.2), marks the transmission dead-lettered, and emits an alert. - Dead-lettered transmissions are surfaced on the reconciliation dashboard (§10.7.3) for human triage: re-drive (after fixing the cause), reclassify as terminal-business, or escalate to B2Brouter support with the stored request id.
- No dead-letter ages past its tax deadline silently — see §10.7.7.
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):
- Outbound drift — invoices Green-Got believes sent but B2Brouter shows pending/failed (or vice-versa).
- Stuck transmissions — non-terminal beyond SLO; dead-letters awaiting triage.
- Status mismatch — Green-Got internal status vs B2Brouter API status vs AFNOR legal status disagree (the three-layer mapping, 5. Lifecycle Statuses).
- Received-invoice coverage — every
ReceivedInvoicepolled/seen has a correspondingbills/document.
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
| Severity | Definition | Examples |
|---|---|---|
| SEV1 | Legal/financial exposure imminent or occurring | A transmission will miss (or has missed) a DGFiP tax deadline; systemic send failure during a reporting window; data-integrity breach in legal status. |
| SEV2 | Degraded but no imminent legal breach | B2Brouter outage with retries holding; webhook processing backlog; reconciliation surfaces drift within deadline margin. |
| SEV3 | Contained / cosmetic | Isolated 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
- RPO = 0 for accepted transmissions. Every accepted “send” is a durable Temporal/queue entry and a persisted domain record before the synchronous call returns; a crash loses no in-flight transmission (the no-loss invariant). Stored request ids and outcomes are persisted transactionally with state changes.
- RTO ≤ 1h for the transmission worker: on infrastructure loss, Temporal workflows resume from their durable state on restart; no transmission is re-driven by hand. Reconciliation (§10.7.3) is run on recovery to confirm no drift was introduced.
- B2Brouter is an external dependency with its own availability; Green-Got’s RTO covers Green-Got’s components — an upstream B2Brouter outage is absorbed by the durable queue, not counted against Green-Got RTO, but is tracked against tax deadlines (§10.7.7).
10.7.6 Replay / backfill
- Replay — reprocessing already-received webhooks/events is safe by construction: handlers are idempotent and dedup-keyed (§5.2), and a webhook only triggers an authoritative refetch. An events backfill (poll the events feed for a window and re-drive handlers) reconciles any missed webhooks.
- Backfill — re-driving outbound transmissions for a window consults the persisted outcome per key (§5.1) and refetches
GET /invoices/{id}before any re-send, so a backfill never double-sends. Backfills run with the same bounded concurrency + rate budget (§2). - Recovery evidence — every replay/backfill run records what it touched (Green-Got ids, resulting states, request ids), producing an auditable trail that the recovery left the system consistent (cross-checked by reconciliation, §10.7.3).
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:
- Deadline tracking — each transmission/tax-report carries its applicable legal deadline; the system computes time-to-deadline continuously.
- 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.
- 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, therequest_log_urlpulled from the short-TTL support vault, §3.2 — for provider-side issues). - 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
- Transient/handled (no customer impact): retries absorbing a B2Brouter outage are not surfaced to the customer — the send already returned as durably accepted (§10.1).
- Terminal business failure (Refusée / Rejetée / validation
422): the customer is notified, because it requires their action (correct and re-issue); surfaced through the normal invoice lifecycle UI (5. Lifecycle Statuses). - Deadline-at-risk or breach (SEV1): the affected customer is proactively notified per the tax-deadline procedure (§10.7.7), with the impact and the remediation in progress.
- All customer-facing notifications reference the invoice by the customer’s own number, never by internal/provider ids.
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
- 4. Sending Invoices — the outbound call sequence this queue drives; uses these limits on the outbound flow.
- 5. Receiving Invoices — uses pagination for listing inbound.
- 6. Statuses and Webhooks — quota note and webhook idempotency.
- 3. Onboarding Accounts — authentication and environments.
- 5. Lifecycle Statuses — the internal error model and outbound status set the transmission workflow maps onto (§10.3).
- Invoicing 9. Transaction Matching — precedent for Temporal as the durable scheduler in this codebase.
12. Sources
- Error codes (incl.
parameter_cannot_change,X-B2B-API-Request-Id,request_log_url): https://docs.b2brouter.net/en/developers/essential-guides/error-codes/ - Introduction (rate limits,
429, versioning, environments): https://docs.b2brouter.net/en/developers/start-using-our-api/introduction/ - DGFiP e-invoicing & e-reporting (tax-report settings, validation flow, import vs create): https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/
- Create invoice (create path,
send_after_import): https://b2brouter.readme.io/reference/create-invoice - Get accounts (query language, pagination): https://developer.b2brouter.net/reference/get-accounts
- Attachments guide: https://developer.b2brouter.net/docs/attachments_guide
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 atest_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_urlfrom 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 / flow | What it answers | Action |
|---|---|---|---|
| A1 | POST /accounts (one account per SIREN) | Exact create body + response shape; how SIREN/identifiers echo back; 422 parameter_taken on duplicate | To capture on staging |
| A2 | GET /accounts?cin_value=<siren> lookup (scheme 0002; SIREN is a cin_* identifier, not tin_*) | Pre-create dedupe lookup response; fields proving an account already exists | To capture on staging |
| A3 | POST /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 defaulted | To capture on staging (Q2, Q3, Q4) |
| A4 | Account/settings response after A3 | Fields proving Annuaire registration and PA designation took effect (and propagation delay) | To capture on staging (Q4) |
| A5 | SIRET / internal-routing configuration attempt | Whether SIRET-level / internal routing scope is settable via API or only via UI/support | To capture on staging (Q5) |
2.2 Invoice creation, validation, send
| # | Path / flow | What it answers | Action |
|---|---|---|---|
| B1 | POST /accounts/{account_id}/invoices (JSON create) | JSON-create request/response; inline validation 422 shape (body + request id + request_log_url) | To capture on staging |
| B2 | POST /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 shape | To capture on staging (Q6) |
| B3 | Create/import with send_after_import: true | Validate-and-send atomically; behavior when validation fails (no transmit) | To capture on staging |
| B4 | send_invoice (send step) | Send response (204?), routing-failure 422 vs validation 422; required vs optional send params | To capture on staging |
| B5 | Legal/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 / flow | What it answers | Action |
|---|---|---|---|
| C1 | Tax-report generation (auto vs manual) | Whether auto_generate / auto_send are required for Flux 1 / Flux 10, and what disabling them implies | To capture on staging (Q3) |
| C2 | GET /tax_reports/{id} (status) | Tax-report status lifecycle values actually returned (new→sent→acknowledged→registered/refused/error) | To capture on staging |
| C3 | Tax-report download | Whether the generated report/ledger is downloadable and in what format | To capture on staging (Q7, Q11) |
| C4 | Flux 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) |
| C5 | Domestic 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 mechanism | To capture on staging (Q10) |
2.4 Contacts and directory
| # | Path / flow | What it answers | Action |
|---|---|---|---|
| D1 | Contact create + contact/directory lookup | Contact create body/response; directory lookup fields used to resolve a recipient before send | To capture on staging |
2.5 Webhooks and received invoices
| # | Path / flow | What it answers | Action |
|---|---|---|---|
| E1 | Webhooks create / list / update / delete | The webhook config CRUD surface and response shapes; signature/secret handling | To capture on staging |
| E2 | Received-invoice polling (GET …/invoices listing) | Whether received French invoices are polling-only or also delivered via webhooks | To capture on staging (Q8) |
| E3 | A delivered webhook payload (any state change) | The actual webhook payload schema + X-B2Brouter-Signature header to verify | To capture on staging |
| E4 | POST /invoices/{id}/mark_as — received French invoices | Confirm 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 invoice | To capture on staging (Q9) |
Note on
mark_asand CDAR. For a received French invoice the production path isaccepted(CDAR 205 Approuvée) /refused(CDAR 210 Refusée) only. The generic B2Broutermark_asreference exposes a barepaid(notallegedly_paid) as a non-France provider capability; Q9 probes on staging whether barepaidis 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 noallegedly_paidreceived target — no provider evidence supports it. Received-side payment status is carried in Green-Got’sbills/(AP) record, not by a B2Brouter receivedmark_as.
2.6 Cross-cutting / audit
| # | Path / flow | What it answers | Action |
|---|---|---|---|
| F1 | Any logged response | Presence of X-B2B-API-Request-Id on success and error; presence of request_log_url on errors | To capture on staging (Q14) |
| F2 | Deliberate 429 (rate-limit) probe | Whether Retry-After / X-RateLimit-* headers are emitted; the support-confirmed concurrency/rate ceiling | To capture on staging (Q12) |
| F3 | API version header behavior | Which API version to pin for prod (X-B2B-API-Version), and behavior on invalid_api_version / missing_api_version | To capture on staging (Q1) |
| F4 | Offboarding archive/export | The 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 confirm | Captured by |
|---|---|---|
| 1 | Which API version (X-B2B-API-Version) do we pin for production? | F3 |
| 2 | What is the exact DGFiP tax-report settings body (required vs optional fields, values)? | A3 |
| 3 | Are auto_generate / auto_send required for Flux 1 / Flux 10, and what does disabling imply? | A3, C1 |
| 4 | Which response fields prove Annuaire registration and PA designation? | A4 |
| 5 | Is SIRET / internal-routing scope configurable via API or only UI/support? | A5 |
| 6 | Is /invoices/import the only legal path for GG-generated XML? | B2 |
| 7 | Is the legal/original artifact downloadable per flow, and in what format? | B5, C3 |
| 8 | Are received French invoices polling-only or also webhooks? | E2 |
| 9 | Confirm 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 |
| 10 | How is domestic B2B VAT-on-collection payment data submitted and corrected — via mark_as (212/allegedly_paid) or POST /accounts/{account}/ledgers/import? | C5 |
| 11 | How are Flux 10 ledgers generated / retried / downloaded / reconciled, and does POST /accounts/{account}/ledgers/import apply to France Flux 10 payment/acquisition reporting? | C4 |
| 12 | What is the support-confirmed rate / concurrency limit (replacing the provisional ~4 throttle, §2)? | F2 |
| 13 | What is the provider’s archive / export guarantee on offboarding? | F4 |
| 14 | Which 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
- 7. API Mechanics — rate limits, error codes, inline validation, idempotency, and the operational-resilience model these captures feed.
- 3. Onboarding Accounts — account creation and DGFiP tax-report settings (rows A1–A4).
- 4. Sending Invoices — create/import/send flow (rows B1–B5).
- 5. Receiving Invoices — received-invoice listing and
mark_as(rows E2, E4). - 6. Statuses and Webhooks — webhook CRUD, signature, events polling, CDAR (rows E1, E3, E4).
- Uncertainties — the documented-gaps register these captures close.
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.
- Class:
provider-support+legal· Owner: Claude (research) → Legal / Compliance + Backend · Due: Before VAT-on-collection customers go live · Launch-blocking: yes - Evidence: 7. E-Reporting, 10. Integration Contracts §11.2
- Status: RESEARCHED (Claude, 2026-06-22) — legal model documented; the issued
mark_as paidtransition exists, but the MEN enrichment + legal effect + correction remain staging/support-gated → launch gate scoped to the MEN, not the transition.
Findings.
- Legal model (documented in canonical docs). For domestic B2B invoiced ops (Flux 1) under VAT-on-collection (CGI art. 290 A), payment data is carried by enriching the AFNOR
Encaissée(212) status with the collection date + amount-by-VAT-rate — the mention d’encaissement (MEN, rule BR-FR-CDV-14); the 212 status is the payment-data channel. For non-invoiced ops (B2C, international B2B) payment data flows via the separate Flux 10 daily Ledgers. Green-Got’s internalPaymentCollectedledger is the data source. This is written into 7. E-Reporting and 5. Lifecycle Statuses. - The paid transition IS documented (correcting an earlier over-statement). B2Brouter’s API reference lists issued-invoice
mark_as {state:"paid"}as a valid target, and the France DGFiP guide listsallegedly_paidas CDAR 212 — i.e. Green-Got commandspaid, observesallegedly_paid. So it is not true that “there is no issuer-side mechanism / it is not available.” Do not state that. - What remains genuinely gated (the launch gate, scoped to the MEN). Unconfirmed before VAT-on-collection launch, pending B2Brouter staging/support: (a) the exact enriched MEN payload fields (collection date + amount by VAT rate) and whether B2Brouter derives them or Green-Got must supply them; (b) the legal effect of the enriched 212 as the art. 290 A payment-data report; (c) the correction/reversal semantics when a reported figure changes. The bare paid transition is available; the MEN-carrying behaviour is the open part. (Note: the received-side
mark_as paid/212 is separately[open — provider-support], P-3 — that restriction is real and unchanged.) - Sources: B2Brouter mark-as reference https://developer.b2brouter.net/reference/mark-as-invoice ; France DGFiP guide https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/.
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.)
- Class:
provider-support· Owner: Claude (research) → Backend / Integration · Due: Before enrollment phase ships · Launch-blocking: yes (the exact FR values only) - Evidence: b2brouter/3. Onboarding §4, 9. Mandate §4.3
- Status: RESEARCHED (Claude, 2026-06-22) — endpoints + core body confirmed; residual enum/flags → staging.
Findings.
- Endpoints (confirmed). Create:
POST /accounts/{account}/tax_report_settings; update:PUT /accounts/{account}/tax_report_settings/{code}with{code} ∈ tbai|sdi|lhdn|verifactu|ksef|dgfip; the FR variant is theDgfipTaxReportSettingschema. Creating thedgfipsetting is the Annuaire-registration trigger and turns on both Flux 1 (per-invoice for Annuaire-registered FR B2B contacts) and Flux 10 (daily-aggregated e-reporting). Annuaire propagation ≈ up to 24h. - Documented body (confirmed verbatim from the DGFiP guide example):
{ "tax_report_setting": { "code": "dgfip", "start_date": "2026-09-01", "type_operation": "services", "naf_code": "62", "enterprise_size": "eti", "email": "…" } }. - Residual (confirm on staging against the
DgfipTaxReportSettingOpenAPI schema): the full enum set fortype_operation(only"services"seen) andenterprise_size(only"eti"seen;micro/pme/eti/ge?), the acceptednaf_codelength (2-digit vs full APE), and whetherenabled/auto_generate/auto_send/profile/formatflags exist (these appeared only in non-verbatim summaries — do not rely on them). - Sources: DGFiP guide https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/ ; https://developer.b2brouter.net/reference/create-tax-report-setting ; https://developer.b2brouter.net/reference/put-tax-report-setting.
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.
- Class:
provider-support· Owner: Claude (research) → Backend / Integration · Due: Before the 2026-09-01 reform date for any SIRET-scoped customer · Launch-blocking: yes (SIRET future state only; SIREN-only MVP is not gated) - Evidence: 6. Annuaire and Routing, 9. Mandate §4.4
- Status: RESEARCHED (Claude, 2026-06-22) — SIREN-level confirmed; per-SIRET routing scope undocumented → staging. Does not gate the SIREN-only MVP.
Findings.
- Confirmed: identifiers exist at both granularities (
cin_scheme "0002"SIREN /"0009"SIRET); registered FR companies are addressed in the Annuaire/Peppol via EAS scheme0225(FRCTC Electronic Address); per-recipient routing is resolved automatically via Directory lookup + the Peppol four-corner model. - Open (the actual P-2 question): there is no publicly documented API field to declare a routing scope finer than the whole SIREN — no documented per-SIRET (establishment-level) routing or internal sub-routing code beneath the EAS identifier within one platform account. The “organisational units / multiple-SIRET account” framing is not a confirmed API feature.
- Resolving staging test: create an account for a SIREN with ≥2 SIRET establishments; attempt SIRET-granularity registration (
cin_scheme "0009"); inspect the Directory lookup + resulting0225Annuaire entries to confirm whether routing can be set per-SIRET or collapses to SIREN-level. SIREN-only MVP is unaffected; the enrollment-time SIREN-only enforcement invariant is in 6. Annuaire §3. - Sources: DGFiP guide https://docs.b2brouter.net/en/developers/guides-by-country/france/dgfip-e-invoicing-and-e-reporting/ ; https://developer.b2brouter.net/docs/setting_up_guide.
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).
- In-app checkbox (FR): « Je désigne Green-Got et son sous-traitant B2Brouter comme ma Plateforme Agréée (PA) et autorise Green-Got à agir en qualité de mandataire pour émettre, recevoir, transmettre et archiver mes factures électroniques et déclarer mes données de e-reporting auprès de l’administration fiscale, conformément aux Conditions Générales d’Utilisation. »
- In-app checkbox (EN): “I designate Green-Got and its subprocessor B2Brouter as my Plateforme Agréée (PA) and authorise Green-Got to act as my agent to issue, receive, transmit and archive my electronic invoices and report my e-reporting data to the tax authority, in accordance with the Terms of Service.”
- TOS clause skeleton (“Mandat Plateforme Agréée”): designation of Green-Got (+ B2Brouter as subprocessor/PA); scope (Flux 1 issuance & transmission, inbound receipt, Flux 10 e-reporting); archiving duration (10 / 6 years); customer’s right to download originals and to a full export; revocation / PA-switch with ≥ 1-year continuity (see L-2); data location; liability.
- Evidence to capture per acceptance: authenticated user id bound to the SIREN, consent-version
string (
pa-mandate-v1), TOS version hash, timestamp, IP. - ⚠️ Legal caveat (your “is this enough?”): a checkbox is a valid electronic consent, but for a mandate that binds the company you also need (a) the acceptor authenticated as an authorised representative of the SIREN, (b) the versioned consent string + TOS hash stored as evidence, and (c) Legal to confirm a checkbox suffices vs. requiring an explicit signed mandate. Treat L-1 as design-decided, legal-sufficiency confirmation outstanding.
- Port to: 9. Mandate §4.1 & §4.5
- Launch-blocking: yes
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):
- Archive availability: indefinite as a product-archive promise — governed by the canonical four-basis retention model, not an unconstrained “never delete”. Per 8. Archiving §2/§5.9 and 13. Privacy §4: the statutory floor (6 yr VAT / 10 yr accounting) is mandatory; beyond it the full PII-bearing artefact is kept under the product-archive-promise basis with customer-exit / DSAR / post-statutory deletion controls; only non-PII integrity evidence (content hashes, PAF chain) is truly permanent. “Indefinite availability” is the product intent, exercised within those controls.
- Misrouted inbound: rerouted / forwarded indefinitely (the routing/continuity obligation; distinct from PII-byte retention above).
- In-flight outbound + e-reporting (Flux 10): continue until there is nothing left to transmit.
- Clean Annuaire deregistration on switch.
→ 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 promise — governed 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.
| # | Item | Launch-blocking | Pinned at |
|---|---|---|---|
| P-6 | “Not the designated PA” exact error code | no | 9. Mandate §5.5 |
| P-7 | Flux 10 ledger generate / retry / download / reconcile | no | 7. E-Reporting |
| P-8 | Support-confirmed rate / concurrency limit (~4 concurrent, ~1000/min prod, ~600/min staging) | no | b2brouter/7. API Mechanics §10 |
| P-9 | B2Brouter API version to pin + deprecation signal | no | b2brouter/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-2 | Webhook payload shape & HMAC digest encoding (hex vs base64) | no | b2brouter/6. Statuses §4 |
| W-3 | ⚠️ Audit-storage exact TTLs (short-TTL support vault + operational logs; auto-expiry verified) | yes | 13. Privacy §4.1 |
| W-4 | CNIL alignment of operational retention durations (logs, contact/prospect window) — cite the specific CNIL référentiel per duration and confirm against current CNIL guidance | no | 13. Privacy §4 |
| R-1 | Daily 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 model | no | 7. 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
- 14. Implementation Wiring — the target wiring these items block/shape.
- 10. Integration Contracts — canonical events, ledger, ComplianceEventLog, PaTransmission.
- 9. Mandate and Onboarding — enrollment, mandate, PA-switch, loss-of-designation.
- 7. E-Reporting — Flux 10 payment data.
- 8. Archiving and Audit — storage and retention.
- 13. Privacy and Data Protection — retention table, B2Brouter DPA.
- invoicing/uncertainties.md — the AR-side active register.
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:
- Knowledge: this is information only the real person would know, such as passwords or secret questions
- Possession: Physical or digital items in the person’s possession, such as a mobile phone receiving SMS, an authentication app, or a hardware token
- Inherence: Biological characteristics unique to the person, such as fingerprints, facial features, or behavioral patterns like typing rhythm.
Modern authentication systems combine these principles with context-aware security:
- Something you use: Trusted devices that have been previously authenticated and verified.
- Somewhere you are: Location data and usage patterns that match expected behavior.
- Something you do: Behaviour patterns that matches your usual behaviour and your cohort behaviour.
- Something you can access: like an email, a physical mailbox, an SMS…
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:
- See card details and IBAN
- Add or edit a beneficiary for SCT
- Accept or revoke an SDD mandate
- Make a wire transfer
- Accept a 3DS validation
- Update card status and limits
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.
-
Device-Based
- Trusted device status
- Device fingerprinting
- Secure storage verification
- Last usage timestamp
- Email OTP
- SMS OTP
- Authenticator apps
- Passkeys
-
Knowledge-Based
- Password
- Secret questions
- Card serial number
- ID document numbers
-
Biometric
- Facial recognition
- Voice recognition
- Behavioral patterns
- Typing patterns
-
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
-
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.
-
Behavioral Analysis
- Usage patterns
- Time patterns
- Input characteristics
- Location patterns
-
Device Management
- Trust level tracking
- Usage history maintenance
- Expiration management
- Activity timestamps
Solution models
Authentication model
Each authentication correspond to an authentication event.
Notes
- Multiple devices possible per customer
- A device appear mulitple times in the database. Each time an authentication was performed with it.
- Device sharing between customers supported
- Historical authentication patterns per customer/device are used for security
- Records all authentication factors used
- Allows real-time risk assessment
- Helps with fraud pattern detection
- Helps with compliance reporting and security incident investigation
Model
-
identifier: Unique authentication identifier -
customer_identifier: Reference to the customer -
initiated_at: Date -
completed_at: Date -
status: Enum:active: can be usedpanic_revoked: revoked by the user with the panic buttonrevoked: revoked by a workflow (hard lock for example)soft_locked: can be unlocked by the internal team or by a workflowexpired: expiredcanceled: the user logged outfailed: the authentication was refused
-
last_update: Date -
trust_level: Enumstrongsimple -
expiration_date: Optional; Date -
authentication_method:- type (password/sms/email/biometric/hardware_token)
- factors_used
- completion_status
-
risk_score: int (0-100) -
failure_reason: Optional; Enum -
device_context:device_identifier: string - unique id stored on the device (can persist or not depending on the OS)device_type: string - mobile / web / tablet…operating_system: stringuser_agent: stringfingerprint: string (not 100% unique)mac_address: string - Device MAC addressimei_number: string - Mobile device IMEIdevice_serial: string - Hardware serial numberdevice_name: string - User-assigned device namescreen_resolution: string - Display resolution (WxH)color_depth: int - Screen color depth in bitsscreen_orientation: enum (portrait/landscape) - Current orientationaspect_ratio: float - Screen aspect ratiorefresh_rate: int - Screen refresh rate in Hzgyroscope_x: float - X-axis rotation in degreesgyroscope_y: float - Y-axis rotation in degreesgyroscope_z: float - Z-axis rotation in degreesaccelerometer_x: float - X-axis acceleration in m/s²accelerometer_y: float - Y-axis acceleration in m/s²accelerometer_z: float - Z-axis acceleration in m/s²ambient_light: int - Light sensor reading in luxproximity_active: boolean - Proximity sensor statusmagnetometer_reading: float - Magnetic field strengthos_version: string - Operating system versionsystem_language: string - OS language settingtimezone: string - System timezoneuptime_seconds: int - System uptimeavailable_memory: int - Free memory in bytesbattery_level: int - Battery percentagebattery_charging: boolean - Charging statuscpu_usage: float - CPU utilization percentage
-
network_data:ip_address: string - IPv4 or IPv6 addressconnection_type: enum (wifi/cellular/ethernet) - Primary network connectionnetwork_operator: string - ISP or mobile carrier namesignal_strength: int - Network signal strength in dBmnetwork_speed: int - Connection speed in Mbpsnetwork_latency: int - Round trip time in msvpn_detected: boolean - Indicates VPN usageproxy_detected: boolean - Indicates proxy usagetor_exit_node: boolean - Identifies TOR exit nodedns_servers: string[] - List of DNS serverstls_version: string - SSL/TLS protocol versioncertificate_fingerprint: string - SSL certificate hashuser_agent: string - Browser user agent stringaccept_language: string - Browser language preferencednt_header: boolean - Do Not Track header statusx_forwarded_for: string[] - Proxy chain IPshttp_referrer: string - Previous page URL
-
browser_app_data:browser_name: string - Browser identificationbrowser_version: string - Browser version numberplugins_list: string[] - Installed browser pluginsextensions_active: string[] - Active browser extensionswebgl_vendor: string - WebGL hardware vendorwebgl_renderer: string - WebGL renderer namecanvas_fingerprint: string - Canvas rendering hashaudio_fingerprint: string - Audio processing hashlocal_storage_size: int - Local storage usage in bytescookies_enabled: boolean - Cookie acceptance statusapp_version: string - Application versionapp_install_date: timestamp - Installation dateapp_last_update: timestamp - Last update timeapp_permissions: string[] - Granted permissionsapp_state: enum (active/background) - Current app state
-
interaction_patterns:typing_speed_avg: float - Average typing speed (chars/sec)key_press_duration_avg: int - Average key hold time (ms)touch_pressure: float - Screen pressure levelswipe_speed_avg: float - Average swipe velocityswipe_length_avg: float - Average swipe distancemouse_speed_avg: float - Average cursor velocitymouse_pattern_hash: string - Mouse movement patternscroll_speed_avg: float - Average scroll velocityface_angle_x: float - Face horizontal angleface_angle_y: float - Face vertical anglevoice_pattern_hash: string - Voice characteristics hashfingerprint_quality: float - Fingerprint read qualityhand_tremor_level: float - Hand stability measuregait_pattern_hash: string - Walking pattern signature
-
environmental_context:gps_latitude: float - GPS latitude coordinategps_longitude: float - GPS longitude coordinategps_altitude: float - Altitude in metersmovement_speed: float - Device velocitymovement_direction: float - Movement heading degreeslocation_accuracy: float - GPS accuracy in metersstreet_address: string - Resolved street addresscity: string - Resolved city namecountry_code: string - Country code (ISO)login_timestamp: timestamp - Authentication timesession_duration: int - Session length in secondslogin_frequency: float - Logins per time periodinter_action_delay: int - Time between actions (ms)typical_hours_start: int - Usually active from (hour)typical_hours_end: int - Usually active until (hour)
-
security_context:is_rooted: boolean - Root/Jailbreak detectedis_emulator: boolean - Emulator detecteddebug_mode: boolean - Debug mode activedeveloper_options: boolean - Developer mode enabledscreen_capture_active: boolean - Screen recording statusauth_method: enum - Primary auth methodauth_factors: string[] - Used auth factorsattempt_sequence: string[] - Auth attempt ordererror_count: int - Authentication errorsrecovery_method: enum - Account recovery type
-
session_context:session_id: string - Unique session identifiersession_start: timestamp - Session start timeactivity_score: float - User activity levelpage_sequence: string[] - Navigation historyfeature_usage: map<string, int> - Feature use countstransaction_count: int - Number of transactionsconcurrent_sessions: int - Active session countdevice_switches: int - Number of device changesparallel_actions: int - Simultaneous activitiescross_device_pattern: string - Multi-device usage hash
-
behavioral_analytics:common_actions: string[] - Frequently used featuresfeature_preferences: map<string, float> - Feature weightstypical_path: string[] - Common navigation patternavg_page_time: int - Average time per page (sec)interaction_frequency: float - Actions per minutelocation_variance: float - Location pattern deviationtiming_variance: float - Timing pattern deviationbehavior_change_score: float - Behavior deviation scorespeed_anomaly_score: float - Speed pattern deviationpattern_break_count: int - Pattern violation count
-
performance_metrics:api_response_time: int - Average API latency (ms)api_error_rate: float - API error percentagememory_usage: float - Memory utilization percenterror_count: int - Total error occurrencescache_hit_ratio: float - Cache effectivenessinput_lag: int - Input delay (ms)page_load_time: int - Page rendering time (ms)animation_fps: float - Animation frame rateinteraction_delay: int - UI response time (ms)error_recovery_time: int - Error resolution time (ms)
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
- Authentication elements must be independent
- Compromise of one element cannot compromise others
- Must be executed in separate domains
- Must implement monitoring for compromise attempts
This is a particularly tricky one. Since the phone contains almost all the informations that we could ask to a person.
Dynamic Linking
- For payments: Authentication code must be specific to:
- Amount
- Payee
- Transaction details
- Any change invalidates the authentication
Security Measures
- Confidentiality and integrity of authentication data
- Encryption of communication channels
- Session management and timeout procedures
- Transaction monitoring mechanisms
Session Requirements
- Maximum authentication time limit
- Session timeout procedures
- Concurrent session handling
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
- Clear user interface
- Explicit confirmation steps
- Error handling and fallback procedures
- Timeout management
Technical Implementation
- Secure storage of credentials
- Encryption standards (minimum TLS 1.2)
- Session management
- Audit logging
Authentication Elements
Knowledge Elements
- Cannot be disclosed in authentication interfaces
- Must implement security measures to prevent guessing
- Cannot be derivable from other displayed information
- Must be protected against unauthorized disclosure
Possession Elements
- Must implement measures to prevent replication
- Must use secure communication channels
- Must ensure only legitimate user has control
- Must be protected against unauthorized use
Inherence Elements
- Must ensure appropriate biometric sensor security
- Must have sufficiently low false acceptance rates
- Must be protected against spoofing and replay attacks
Authentication Codes
- Length and complexity requirements
- Time validity restrictions
- One-time use only
- Generated using secure algorithms
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
createwill use the local version of theConfigstruct so we can properly validate the config you like to create.- The
createandupdatecommands takes a JSON representation of the parts of the config that you would like to update. For eaxmple to update the inin_memory_analytics_processingfeature flag the following command will need to be invokedgg config create production { "feature_flags": { in_memory_analytics_processing: false } }. We then merge this JSON with the latest configuration in the database to create a new config
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
- Postgres 16
- Sqlx
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:
- Update origin
- Reason for change
- Additional context
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
The issue here is cargo script only works when defining per file dependencies – not what we want
- 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
- 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
- Enabled logging by outputting our traces to the stdout
- Enable us to write async tests
- Enabled us to write tests that interact with the database
- 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
- Use-Case Catalogue — the cross-crate functional source of
truth. Every business rule in a crate’s docs and every test traces back to a
UC-<AREA>-NNid here. Read this first. - Glossary — shared vocabulary (Offer, Module, Step, Subscription, Account, Holder/Participant, DataBag, billing objects).
Crates
| Crate | Status | What it owns | Docs |
|---|---|---|---|
| offers | implemented (v1 slice) | Offer / Module catalogue / Step dictionary + composition, eligibility, visibility, lifecycle | offers/docs |
| onboarding | implemented (v1 slice) | the consolidated cross-segment onboarding orchestrator (data-driven Step engine, validation, provisioning) | onboarding/docs |
| subscriptions | design / scaffolding | holder↔offer relation, offer changes, bundles, snapshot+filiation, lifecycle | subscriptions/docs |
| customer_billing | design / scaffolding | Green-Got’s own customer billing subledger (BillableEvent/Invoice/CreditNote/Payment/Refund/Statement) | customer_billing/docs |
| accounting | design / scaffolding | revenue 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.
Pperson ·Aaccount (owned by one holder; a shared account = an account with a participant, not a co-owned/joint account) ·Ccard ·Mmodule ·Ooffer ·Ssubscription ·Iinvoice ·CNcredit note ·BEbillable event.- A subscription is written
S = (holder, offer, {modules}, start → end, status). - “Module” = atomic functional unit (payment account / current-account UX, shared account, card, livret, ASV, PER). “Step” = an onboarding requirement (id-doc, OTP, €50 topup, recap). The two are distinct.
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):
| Entity | What it is |
|---|---|
| Offer | priced commercial SKU; composes Modules ([UC-OFF-14]) + declares required Steps ([UC-ONB-02]); immutable versions ([UC-OFF-06]) |
| Module | atomic functional unit from the Module catalogue ([UC-OFF-14]) — payment account, shared account, card (sub-module), livret, ASV, PER |
| Step | a reusable onboarding requirement from the Step dictionary ([UC-ONB-02]) |
| Subscription | living holder↔offer relation; snapshots offer/module versions + price ([UC-SUB-32]); has dates, cadence, status, customer modifiers ([UC-SUB-29]) |
| Account | the banking object (IBAN, balance, ring-fence); tier-agnostic; ≥1 per bundle subscription ([UC-SUB-12]) |
| Card | sub-module of an account ([UC-SUB-16]) |
| Holder / Participant | the holder owns the account + money; a participant is an authorized user ([UC-ONB-12]/[UC-SUB-26]) |
| BillableEvent / Invoice / CreditNote / Refund / Statement | billing-subledger objects (§5) |
| DataBag | transient per-onboarding working state ([UC-ONB-26]) |
| Onboarding owner | the identity a session belongs to — an anonymous prospect token or an authenticated user ([UC-ONB-28]) |
Domain boundaries — what commercial_domain owns vs references:
| Owns | References (owned elsewhere) |
|---|---|
| Offers / Modules / Steps catalogue; eligibility; versioning | the banking account / IBAN / ledger (core_banking) |
| Subscriptions + lifecycle; offer changes; bundles | identity / 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
- Actor: prospect with no Green-Got relationship.
- Trigger: from the logged-out “open an account” entry, picks the Premium payment-account / current-account offer; the onboarding runs anonymously (owned by a prospect token) until validation promotes the prospect to a real customer ([UC-ONB-28]/[UC-ONB-29]).
- Flow:
- App calls the “subscribable offers” route ([UC-ONB-18]); Premium and Essential are offered (no collision — prospect holds nothing). The customer chooses Premium.
- The app requests an onboarding for the chosen offer; the backend creates it and the
engine resolves required Steps from the offer (
get_emailfirst — verifying the email and minting an authenticated session ([UC-ONB-31]) — thenget_phone(verify phone),personal info,id-doc / PVID,€50 topup intent / receipt,recap) ([UC-ONB-02], [UC-ONB-03], [UC-ONB-20], [UC-ONB-31]). - Customer is walked step-by-step; each visit redirects to the next missing Step
(
/{onboardingId}?step=<next-missing>), so the customer can drop the onboarding and resume it later exactly where they left off ([UC-ONB-03], [UC-ONB-17]). PVID success is non-editable at recap ([UC-ONB-04]). - Onboarding submitted → compliance officer reviews. The officer may invalidate specific Steps; only those are re-requested from the customer — all other already-captured data stays in the onboarding ([UC-ONB-06]). On full validation, proceed ([UC-ONB-05]).
- Validation provisions
P, payment accountA, cardC, and createsS = (P, Premium, {current_account, card}, t0 → ∞, active)via the segment Provisioner ([UC-SUB-18]). Subscription period starts at validationt0. - Provision-before-charge ordering ([UC-BIL-01], [UC-BIL-05], [UC-REG-04]): before KYC validation there is no customer payment-account balance. The customer may send the €50; it lands in Green-Got’s settlement account and is recorded as a pending onboarding top-up, swept to safeguarded liquidity on the normal settlement cadence ([UC-ONB-20]). On validation, the sequence is — (a) create the customer payment account and its safeguarded sub-ledger position, (b) credit it with the pending €50, (c) issue the first invoice for the period, (d) collect it by internal transfer customer sub-ledger → Green-Got operating account. If onboarding fails/expires, the pending top-up is refunded or held for compliance review ([UC-ONB-22]).
- Accounting recognises the fee over the period; the unearned part sits in deferred revenue ([UC-ACC-01]).
- End state: active Premium subscriber, one open/paid invoice, revenue recognising.
UC-J-02 — Brand-new customer, from nothing, to an active free offer
- Trigger: picks the Free plan.
- Flow: as J-01 but the offer’s Steps may omit the topup; on officer validation (or auto-validation where policy allows) the account activates without any payment; no recurring invoice is produced ([UC-BIL-13]).
- End state: active free subscriber, no billing.
UC-J-03 — Existing customer subscribes to a second offer (onboarding from an existing account)
- Actor: active customer holding
S1 = (P, Essential, {current_account, card}, active). - Trigger: subscribes to a Livret offer.
- Flow:
- Subscribable-offers route returns Livret (dependency satisfied: Livret requires a payment account, which P has) ([UC-SUB-11]).
- Onboarding for Livret reuses P’s identity via DataBag pre-fill — only the genuine gaps are asked (funds-origin, livret consent); id-doc is not re-run ([UC-ONB-10]).
- Validation provisions the livret (references an
investmentcontract) and createsS2 = (P, Livret, {livret}, active)— a second subscription, different module, no collision ([UC-SUB-02], [UC-SUB-15]).
- End state: P holds two subscriptions; the livret is free → no billing line; any Green-Got revenue is partner commission, accounted only ([UC-ACC-07]).
UC-J-04 — Existing customer upgrades (product migration), mid-period
- Actor:
S1 = (P, Essential, active), billed monthly on the 1st, today is the 16th. - Trigger: upgrades to Premium.
- Flow: immediate close-then-open ([UC-SUB-06]);
S1closes,S2 = (P, Premium, switch → ∞, active)opens with its own anniversary at the switch date; the invoice nets a full-price new line against a prorata credit for the old sub ([UC-BIL-08]); recognition auto-lands ([UC-ACC-01]); Premium perks available immediately. - End state: active Premium;
S1retained, expired, linked via filiation.
UC-J-05 — Legacy customer migrated from agent-era plan to a new independent-PI offer
- Actor: existing customer on the agent-era “Premium” plan, holding a payment account + card.
- Trigger: user-led — the customer activates their (new) card to finalise their migration. There is no silent bulk backend cutover; each customer migrates at their own pace within the migration window.
- Flow: mechanically an automated onboarding ([UC-MIG-03]): tick the new-T&Cs box → build a DataBag from existing data → provision new entities, incl. a new IBAN (best-effort mapping [UC-MIG-03a]; price set to stay the same [UC-MIG-03d]). Old-account close + money-move happen on the legacy platform — out of scope ([UC-MIG-03c]); continuity hooks in [UC-MIG-03o]–[UC-MIG-03r].
- End state: customer is on the new independent-PI offer with a new IBAN, no service interruption.
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)
- segment.
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:
- if the user has no current subscription → show the onboarding directly (routed to step X);
- if the user already has products → show a homepage of their current products plus a link to continue the ongoing onboarding.
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:
- on your first connection, doing the initial tunnel first-try;
- resuming after dropping out 10 days ago; or
- returning after a partial rejection, where the officer invalidated specific Step(s).
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:
- normal rejection / expiry → refund to the original source where possible, or to a verified payout account collected for that purpose;
- suspicious funds / AML alert → do not auto-refund; hold and route to compliance, respecting no-tipping-off constraints ([UC-REG-02]).
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:
- config — which offer(s) to surface / which onboarding to launch, and any attached promo, prime ([UC-REG-10]), or whitelist entry ([UC-OFF-12]);
- validity window — a start date and an end date; usable only within
[start, end]; - max usage count — a total redemption cap (e.g. “the first 1 000 people”); once reached the code is exhausted;
- per-person use — single-use per person or reusable (a config flag).
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:
VALENTINE2027— surfaces the Valentine bundle; valid 2027-02-01 → 2027-02-28; unlimited uses.LAUNCH1000— €50 prime ([UC-REG-10]) on signup; valid from launch day; max 1 000 redemptions, then exhausted.AMBASSADOR-ACME— whitelists the holder for the price-0 ambassador offer ([UC-ONB-09]); single-use per person; no end date.- Event-card QR ([UC-ONB-19]) — a per-card code, single-use, bound to one pre-created card.
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:
- Anonymous prospect — started from the unauthenticated / logged-out surface (“open an
account”). The backend creates a prospect session — a freshly minted prospect token carried as an
httpOnly cookie — that is the sole handle to the session; the DataBag starts empty ([UC-ONB-10],
[UC-ONB-26]). This anonymous phase is short-lived: it lasts only until the
get_emailStep verifies the email, at which point the backend creates the user and promotes the owner to thatuser_id, retiring the prospect token ([UC-ONB-31]). Even so, no person, account or card exists until validation, so the session grants no access to any real data throughout. - Authenticated user — started from inside the app by a logged-in user (e.g. an existing
customer subscribing to a second offer, [UC-J-03]). Owned by
user_idfrom the session; the DataBag is pre-filled from canonical KYC ([UC-ONB-10]).
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:
- 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 theuser_id; the prospect token is retired. - 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 atget_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:
get_email(first). Captures the prospect’s email and runs the duplicate-identity check on the email ([UC-ONB-32]). On a fresh identity it issues a one-time code by email (a stubbed email sender for now; an OTP code, not a magic link) and stays unmet until the code is verified. On a correct code it (a) creates a real minimal user with a verified email login-identifier, (b) mints an authenticated session — exactly as a successful login would (passwordless: the session records anEmailOtpcredential and is delivered as the standard httpOnly session cookie / token), (c) promotes the onboarding owner from the prospect token to the newuser_id([UC-ONB-28]), and recordsEmailVerifiedin the bag. From this Step onward the caller is authenticated, not anonymous.get_phone(second). Captures the phone and runs the duplicate-identity check on the phone ([UC-ONB-32]). On a fresh identity it issues a one-time code by SMS and, on a correct code, adds a verified phone login-identifier to the now-authenticated user and recordsPhoneVerified. The phone channel is selected by the request channel ([UC-ONB-33]): web → Vonage OTP (stubbed for now); mobile → Infobip silent network authentication (deferred — behind the same provider seam). No new session is minted here; the email Step already established it.
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 Step —
one 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:
- Fresh identity → the channel receives the onboarding OTP; entering it verifies and advances.
- Existing identity → the channel instead receives a “you already have an account — log in / recover access” message (a login link; optionally “someone tried to start onboarding with your address”). No onboarding code is issued, so the Step cannot be completed and the genuine owner is routed to login.
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.
- Server meta (authoritative, never client-trusted) — captured backend-side on each step
submission: the request IP, geolocation (city / region / country / lat-long / ASN, from
the CDN edge, mirroring the
AuthRequestContextthe authentication domain already records), the device (the existingdeviceid + type — web cookie / mobile id,devicedomain), the user-agent, the step code, and the keys of the DataPoints written by the step. Raw regulated values are not duplicated into the log — the DataBag/canonical record remains the source of truth, and secret markers (OTP hashes, any CHD/SAD) are never written to an event ([UC-REG-08]) — so archival/erasure of the identity bootstrap ([UC-ONB-15]) is not undermined. - Client meta (behavioural, obfuscated, best-effort) — the front end instruments each view and
packs per-view behavioural signals: view dwell duration, tab-switch / visibility changes,
copy and paste counts, focus/blur, and the view code. It is sent obfuscated (packed +
base64 in an
x-onb-metaheader on every step submission) — deliberately not signed this round; it is advisory only and explicitly untrusted (a determined actor can forge it), which is why it never gates a decision and lives beside, not inside, the authoritative server meta.
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:
- Complete-primary — an unfinished first onboarding (in progress, or an officer sent changes back, [UC-ONB-06]) and no account yet: the app redirects to the onboarding and allows nothing else — there is genuinely nothing else they can do. (Instant client-side redirect.)
- Awaiting-first-validation — the first onboarding is submitted / under review ([UC-ONB-25]), still no account: the app is browsable, but shows a non-dismissable “account being reviewed, please wait” banner.
- Complete-secondary — a validated customer ([UC-ONB-05]) has a second onboarding ([UC-J-03]) that needs work (in progress / changes requested): the app works normally, plus a non-dismissable “finish this onboarding” banner with a CTA.
- Secondary-awaiting-validation — a validated customer’s second onboarding is under review: normal app plus an informational banner.
- None — a validated customer with nothing pending: normal app.
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 offer → one 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:
- Contractual basis — immutable, versioned. Price, module composition (what the customer gets), and key contractual terms/entitlements are append-only: a change is a new version row, never a mutation. Moving the existing base onto the new version is an offer change / forced migration ([UC-SUB-06]/[UC-SUB-17]), routed through framework-change notice where it raises price or reduces rights ([UC-REG-01]). This guarantees every invoice links to an immutable, fixed-price offer, and live subscriptions reference a frozen version ([UC-SUB-32]). In particular there is deliberately no “edit the price of a live offer” — raising the price is always a new offer (e.g. Essential 2028).
- Non-material fields — editable in place. Presentation and non-contractual detail — selection-screen copy, marketing description, partner-benefit wording, perk presentation — can be edited on the live offer without a new version; the edit applies to everyone on it (including grandfathered subscriptions), which is fine because nothing the customer gets or pays changes. These fields are not part of the subscription snapshot ([UC-SUB-32]) — they’re read live.
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 publicly accessible offer is surfaced to anyone passing eligibility, collision, country and segment checks ([UC-ONB-01]/[UC-ONB-18]).
- A restricted offer (
publicly_accessible = false) is never shown by default; it is surfaced only to whitelisted identities. A whitelist maps an email/phone to the subset of restricted offers it may access. Using a whitelisted email/phone at the subscribable-offers gate makes those restricted offers visible and subscribable; everyone else has no access to them.
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:
- category — exclusivity class (e.g.
current_account), used for collision; - per-person cap / uniqueness — e.g. one
current_account, one of each savings product per person at Green-Got ([UC-SUB-11]); - dependencies — required other modules (livret/ASV/PER require a
current_account, [UC-SUB-11]); - tier — Essential/Premium where applicable, driving entitlements;
- tax_code — VAT/tax classification ([UC-ACC-08]);
- fallback — standard unit offer to fall back to, or
selffor a primitive ([UC-OFF-07]/[UC-OFF-13]); - regulated nature — banking / intermediated-savings (IOBSP) / insurance (IAS), routing conduct rules ([UC-REG-06]).
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:
- prepay-for-discount with pro-rata refund — bill the year upfront at a reduced rate; on early resignation, refund the unused months (N26 model); or
- discount clawback — give the annual rate as a discount off the monthly price; on early exit, re-rate the consumed months at the standard monthly price and keep the difference (Revolut model).
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:
- one account, several cards — typically one physical + N virtual cards, and/or a participant’s own card ([UC-SUB-26]); each card carries its own product/tier (Standard/Premium, virtual/physical);
- several accounts, one card each — a holder with multiple accounts ([UC-SUB-12]) may hold one card per account.
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:
- Simple migration (we have everything): a scheduled offer change to the declared fallback on the same account (close-then-open, filiation, [UC-OFF-11]); perks/billing follow the new offer; no commercial refund. Used for deterministic-boundary cases needing no new data (e.g. a fixed-term offer’s end date reverting to standard — distinct from an intrinsic price schedule, [UC-OFF-03]).
- Minor → adult (a youth participant turns 18, [UC-ONB-11]): under the participant-on-parent’s-account model there is no forced transition — the minor simply becomes an adult participant, i.e. a normal (if unusual) shared account between a parent and their adult child ([UC-ONB-12]). The money was always the parent’s, so nothing must move. Age-gated participant limits set by the parent ([UC-SUB-26]) may relax, but nothing is forced; the adult may choose to open their own account ([UC-ONB-01]), and the parent may revoke access or transfer funds at will. (A parent + adult-child shared account is admittedly unusual but permitted — no rush to special-case it.)
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:
- payment means are blocked (no spending / transfers);
- the user can still consult and download account details and statements.
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:
- monthly — effective at the end of the current paid period; subscription stays active and usable until the boundary; no voluntary unused-time refund (perks kept to the boundary);
- annual — resignable at will; the unused prepaid months are refunded pro-rata (or the discount clawed back) per [UC-SUB-09], because the year was prepaid, not committed.
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:
- Projected balance (display only, not credit). Alongside the real available balance, the app shows a prevision line reflecting pending movements the customer hasn’t yet felt (an announced incoming transfer, an upcoming SDD debit). No value has moved, no one is owed, it is not a ledger position and not credit — pure UI anticipation.
- Settled negative position (real receivable). 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 acknowledged as de facto short-term ancillary credit under PSD2 art. 18(4) — involuntary
(forced by an offline/STIP authorisation), short-term, ancillary to executing a card payment, and not
funded from safeguarded client money. Art. 18 ancillary credit also carries a ≤12-month repayment
ceiling and PI own-funds / credit-granting conditions: the position must be repaid within that
ceiling and may only be granted within the PI’s permitted ancillary-credit envelope (own funds, never
safeguarded client money). To stay inside that envelope it is bounded: a per-account
cap (beyond it the account is restricted, no further authorisations); recovered from the next
incoming funds; if it persists beyond
[N days]→ dunning → restriction → write-off ([UC-BIL-10]); no revolving/rollover, never offered or advertised as available funds. It is booked as a real debit/receivable, distinct from the projected display.
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 fees — frais 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:
- Expiry → automatic renewal — a new card is reissued at expiry free of charge; no BillableEvent.
- Lost / stolen / damaged → reissue — the replacement carries a reissue fee ([UC-BIL-04]), a BillableEvent on the customer’s invoice. PIN reissue follows the fee schedule.
- Tier change (Standard ↔ Premium) — a module-level change within the subscription; a tier upgrade may be a paid offer change ([UC-SUB-06]). Tier lives on the module ([UC-SUB-16]).
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:
- tags — e.g.
free(free-for-life),employee— that zero or alter the recurring charge. Set at activation (from a whitelist/code, [UC-ONB-09]) so a comp applies from the first invoice — no pay-then-reimburse. This is the canonical way to give an ambassador/employee a paid offer for free; - free-month counter ([UC-BIL-17]) — increments from parrainage ([UC-BIL-20]), compensation gestures ([UC-BIL-07]), or promos; decrements at each cycle;
- promo-code discounts ([UC-BIL-19]) — a redeemed code attaches a customer-specific discount or free months to the subscription.
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:
- effective date — on request, subject to a contractual notice capped at 30 days;
- pro-rata prepaid-fee refund — any prepaid regular charge (monthly or annual) is refunded pro-rata to the effective termination date, via the refund machinery ([UC-BIL-09]);
- non-waivable — overrides the commercial “no unused-time refund” default ([UC-SUB-23]) and the framework-change paths ([UC-REG-01]).
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:
- the offer_version and each referenced module_version;
- the price (
price_snapshot) and the key contract terms / fee-schedule version delivered; - the billing cadence and anchor.
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:
- Offer — catalogue lifecycle: draft → active (within
[start, end]) → retired ([UC-OFF-05]); immutable versions ([UC-OFF-06]). - Subscription — the holder↔offer relation: pending → active → (suspended / arrears) → expired/closed; an offer change ends one and opens another ([UC-SUB-12]).
- Account — the banking object: active → suspended (AML, [UC-SUB-22]) / closing → archived ([UC-SUB-19]); it can outlive its subscription (closing wind-down) and is tier-agnostic ([UC-SUB-16]).
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:
- Error correction (irreducible). We charged a fee in error, or charged it twice (bug, retry race). Returning money taken in error is legally required — “no refund” cannot override our own mistake. This is one of several non-waivable refund paths, alongside withdrawal/renunciation ([UC-BIL-14]), statutory payment-service refunds ([UC-REG-03]), and framework-termination pro-rata ([UC-REG-01]).
- Payment-service statutory refunds / restorations ([UC-REG-03]). Unauthorised, non-executed, defective, or otherwise refundable payment transactions are handled in the payment/disputes domain; commercial billing records the resulting credit note/refund impact where fees were charged.
- Framework-contract termination / change rights ([UC-REG-01]). If a non-waivable rule requires a pro-rata reimbursement of prepaid charges, billing must support it even if the commercial policy tries to make cancellation boundary-effective.
- Cooling-off / rétractation ([UC-BIL-14]) where applicable.
- Pending onboarding top-up return ([UC-ONB-22]). This is return of unactivated funds, not a subscription refund.
- A gesture that cannot be netted ([UC-BIL-07]). A gesture credit note on an already-paid invoice with no future invoice to offset (e.g. a leaving customer) becomes a refund liability. Mostly avoided by netting against the next charge; the residual forces cash back.
- Bundle-merge mid-period ([UC-SUB-21]) — a price drop could net to money owed back; avoided by merging at the period boundary (no mid-period proration).
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:
- Payment-account/card subscription — 14-day distance-financial-services withdrawal (C. consommation L222-7 et seq.). Within the window, reverse the activation fee (full or pro-rata per regime) via [UC-BIL-09].
- ASV / life products — 30-day renunciation (C. assurances L132-5-1); PER per its own regime. Renunciation triggers partner commission clawback ([UC-ACC-15]), not a customer-invoice refund.
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:
- service not rendered (e.g. card not delivered) → credit note reducing revenue; if already collected, creates a refund liability until paid;
- gesture reducing an invoice → credit note reducing revenue;
- cashback / bonus / cash reward not linked to an invoice → customer credit / pending balance liability, not an invoice credit note;
- pure compensation / indemnity not reducing a price → expense, with customer liability until settled;
- billing error / double charge → correction credit note and refund liability if already collected.
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:
- E-invoicing (invoice transmission). Mandatory domestic B2B only — invoices to French VAT-taxable business customers are transmitted via a PDP ([UC-BIL-12]). B2C consumer invoices are never transmitted to the individual.
- E-reporting (transaction-data transmission). For VAT-taxable B2C revenue we transmit aggregated daily transaction data (amount invoiced + VAT collected) to the tax authority via a PDP — not an invoice to the customer. VAT-exempt payment-service fees (art. 261 C, [UC-ACC-08]) are out of e-reporting scope; the VAT-taxable subscription (260 B option) is in scope.
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:
- a withdrawal function available throughout the withdrawal period ([UC-BIL-14]);
- a durable-medium acknowledgement of receipt of the withdrawal;
- refund within the statutory SLA (without undue delay, ≤ 30 days);
- proof of precontractual-information delivery on a durable medium — if not delivered, the withdrawal period is extended.
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”:
- business-card interchange (~2%) — commercial-card interchange is uncapped and can fund rewards on business spend (consumer debit interchange is capped at 0.2% under the IFR 2015/751 and cannot fund meaningful cashback — so “interchange-funded” is false for consumer cards);
- partner-funded cashback — merchant/partner contracts pay the reward: e.g. Premium users get 1% at SNCF funded by SNCF and 4% at Biocoop funded by Biocoop; the partner bears the cost and Green-Got passes it through.
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:
- core banking / payment services are within the art. 261 C exemption perimeter;
- Green-Got’s subscription is provisionally treated as taxable at 20% under the art. 260 B VAT option — ⚠️ needs tax-advisor validation, not a settled fact: 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 before baking it into accounting;
- non-banking service fees are taxable by default unless their own tax code says otherwise;
- partner commissions on ASV/PER/livrets are treated as VAT-exempt intermediation revenue;
- TCA, where relevant, is borne by the insurer/partner, not by Green-Got; ASV/capitalisation are expected to be outside Green-Got TCA handling.
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)
- Context: Green-Got is moving from operating as a payment-institution agent (under a principal licensed institution) to being an independent licensed Payment Institution. The existing customer base, currently on agent-era plans, must move to the new independent-PI offer catalogue with minimal disruption.
- Trigger & pace: user-led — each customer finalises their own migration by activating their new card; there is no silent bulk cutover. Migration runs per-customer across a window, coexisting with normal operations.
- Mapping: legacy plan → new offer is best-effort; a perfect 1:1 is not guaranteed and some attributes (price, perks, cycle) may change ([UC-MIG-03a]).
- Scope: controlled, auditable, and per-customer reversible until the customer’s activation completes.
- Mechanism — it’s an automated onboarding, not a data migration. When the customer agrees (T&Cs at activation, [UC-MIG-03b]), we build a DataBag from their existing data and run the automated onboarding ([UC-ONB-10] pre-fill); on validation (server or compliance) we create all new entities — reusing the existing physical person, provisioning a new payment account with a NEW IBAN, new card(s), and a new subscription under the new catalogue ([UC-ONB-05] provisions only what’s missing). Closing the previous account and moving the money to the new one happen on the legacy (agent-era) platform — out of scope here. This is the same engine as [UC-ONB-10] and the minor→adult re-onboarding ([UC-SUB-20]).
Sub-cases (each gets rules + tests):
- UC-MIG-03a — Plan→offer mapping (best-effort). Each legacy plan maps to the closest target new offer (free→free, Essential→Essential, Premium→Premium, annual→annual, shared, bundles), but the mapping is not guaranteed lossless — some attributes (price, perks, cycle) may change. The target design is no material customer detriment: migrated offers are priced to keep the customer’s price the same where possible ([UC-MIG-03d]). If a mapped change materially increases price or reduces rights, it must follow the framework-contract change / rejection path ([UC-REG-01]) rather than being hidden inside activation. Plans with no clean target are marginal and handled case-by-case by customer support.
- UC-MIG-03b — Re-consent / new T&Cs. The customer accepts new T&Cs (new legal counterparty) as part of the card-activation migration step — in-app acceptance is sufficient only if the acceptance record is durable, attributable, timestamped, versioned, and reproducible for the customer/back office. Deadline / non-acceptance handling is [UC-MIG-03l].
- UC-MIG-03c — Account / IBAN. A new IBAN is issued — the new payment account is freshly provisioned by the automated onboarding, not the old account renamed. The old account’s wind-down (incoming-payment handling, closure) and the money move to the new account run on the legacy platform — out of scope here. Bank mobility — re-pointing standing SEPA mandates / direct debits to the new IBAN (aide à la mobilité bancaire, CMF L312-1-7) — is a real obligation handled by a separate team / process, and is out of scope for this document (not ignored, just owned elsewhere). The IBAN necessarily changes: Green-Got issues its own IBANs via its own CIB and cannot operate another bank’s IBANs.
- UC-MIG-03d — Pricing. Migrated customers get the new offer’s price (no grandfathered snapshot of the old price) — but the new offers are priced to keep the customer’s price the same, so in practice it does not change.
- UC-MIG-03e — Billing continuity. No double-charge across the cutover; the new subscription’s anchor either preserves the old cycle or resets, with the period bridged so no day is billed twice or skipped. Deferred revenue from the old plan is carried/closed correctly in accounting.
- UC-MIG-03f — In-flight subscriptions & pending changes. A customer with a pending downgrade (or a recent upgrade) at cutover: the pending change is honoured/translated under the new offer.
- UC-MIG-03g — Mid-onboarding at cutover. A prospect onboarding to an agent-era offer when the cutover happens is redirected to the equivalent new offer without restarting completed Steps.
- UC-MIG-03h — Savings / ASV holders. Intermediated products are largely unaffected (no customer billing). But where the distributor / legal counterparty, funding IBAN, mandate, or customer documents change as part of the move, the partner products may need re-papering or at least disclosures ([UC-REG-06]); “no re-papering” holds only when none of those change.
- UC-MIG-03i — Outstanding balances. Open/overdue invoices at cutover are carried over; receivables continuity preserved ([UC-ACC-11]).
- UC-MIG-03j — Free-plan customers. Migrated to the new free offer; no billing impact; re-consent still required ([UC-MIG-03b]).
- UC-MIG-03k — Shared-account customers. The holder must re-consent. Participants must accept new participant terms if their access/card rights continue on the new Green-Got PI platform. The shared subscription is migrated as a unit only when holder consent, participant rights, and card linkage are all resolved.
- UC-MIG-03l — Non-re-consent. Customers who don’t migrate (never accept the new T&Cs) remain customers of PPS — the previous parent payment institution / agent-era principal — on the legacy platform. PPS bears 100% of the liability for those accounts (explicitly agreed with PPS, who have confirmed they will not keep operating indefinitely and will run their own wind-down); Green-Got no longer operates them. Closure, fund recovery, old-IBAN handling, disputes, and statements for non-migrants are entirely PPS’s responsibility (the customer goes to PPS to close and recover their money) — none of it is a Green-Got concern. Green-Got’s independent-PI base is just those who migrated.
- UC-MIG-03m — Two platforms, not coexistence. There is no coexistence within one system: the legacy account lives on the PPS platform, the new account on Green-Got’s independent-PI platform — separate systems. Migration is per-customer; until the customer completes the automated onboarding, nothing is created on our side and they remain wholly on PPS, so it is inherently reversible up to validation (we only provision on validation).
- UC-MIG-03n — Historical data: a separate read-only legacy ledger. Creating the account from the DataBag covers identity/commercial data. PPS-era transactions and statements are not booked into Green-Got’s ledger — they are copied into a separate “legacy migration” ledger that is read-only (frozen once all migrations complete), shown in the customer interface for continuity/information only — it does nothing functionally. The customer’s first transaction in the real Green-Got ledger is the inbound money-move that funds the new account (their last PPS transaction is the matching −X outbound; their first GG transaction is +X). Mapping the legacy ↔ new statement/transaction formats is the work item; copying another PI’s history needs its own lawful basis and an integrity caveat (migrated ≠ natively booked) ([UC-REG-08]).
- UC-MIG-03o — Historical statements and customer access. Migrated old statements are accessible on the new platform (served by Green-Got post-migration, via the mapped data of [UC-MIG-03n]); the continuity contract states where the customer finds them and for how long.
- UC-MIG-03p — Legacy disputes / chargebacks. A transaction made on the PPS platform can be disputed after the customer migrated. The commercial domain only records the customer relationship and any billing/refund consequence; the operational dispute remains with the platform/scheme owner, but the customer support route must not dead-end.
- UC-MIG-03q — Incoming payments to the old IBAN. The legacy platform owns old-IBAN wind-down, but migration must define the user-facing behavior: redirect where possible, reject/return, or notify the sender/customer. This is out of scope for commercial billing, not out of scope for the migration customer journey.
- UC-MIG-03r — Proof of new T&Cs / fee schedule. For each migrated customer, store the accepted T&Cs version, fee schedule, timestamp, channel, and evidence payload so BO/compliance can prove what was accepted and what was delivered on a durable medium ([UC-REG-01]).
- UC-MIG-03s — Started-but-failed migration. A customer who begins the automated onboarding but does not complete/validate it: nothing is provisioned on our side ([UC-MIG-03m] reversibility), they remain on PPS ([UC-MIG-03l]), and the half-done onboarding is archived per [UC-ONB-15]. They may retry within the migration window; recurring failures route to customer support.
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:
- sanctions/asset-freeze hits and terrorism-financing prohibitions;
- AML/CTF minimum controls, no-tipping-off constraints, and compliance-ordered freezes/closures;
- KYC/KYB minimum identity, beneficial-owner, and representative checks;
- age, legal capacity, legal-representative authority, death/incapacity constraints;
- country/residence/licence perimeter and product distribution permissions;
- tax residency / FATCA / CRS data required to open or maintain the product;
- payment-institution perimeter (payment account only, no deposit-taking by Green-Got);
- product-specific regulatory caps or partner-imposed hard eligibility constraints;
- court orders, legal seizures, and mandatory regulatory reporting;
- scheme/network mandatory blocks or dispute outcomes.
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:
- Onboarding events — the append-only timeline ([UC-ONB-35]): per event, the timestamp, step, DataPoints written, and the decoded server + client meta (IP, country, device, dwell time, tab-switches, paste count). Read-only; never editable (audit integrity, [UC-BO-10]).
- PEP — the self-declaration ([UC-ONB-36]): is-PEP, category/public function, family-member, close-associate — surfaced for the officer’s enhanced-due-diligence decision ([UC-REG-02]).
- Screening (out of scope) — a placeholder for sanctions/watchlist screening (OFAC, UN, EU) under [UC-REG-02]; shown as “not yet wired” so the officer knows screening is a separate, not-yet-integrated regime — no backend this build.
- Global signals (derived) — computed by aggregating the onboarding’s events, not stored separately: total onboarding duration, the list of devices used (by their web-cookie / mobile id), whether the device changed mid-onboarding, whether any device was already used by a previous onboarding and links to those onboardings (device-reuse), the arrays of IPs, browsers/user-agents, and countries seen (with a flag per country from its ISO code — the CDN already resolves IP→country, [UC-ONB-35]). Everything is an array because an onboarding may legitimately span multiple devices/IPs/countries ([UC-ONB-17] cross-device resume).
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:
- delivery of T&Cs, fee schedules, and material changes on a durable medium;
- proof of the exact version accepted/delivered (customer, channel, timestamp, content hash/version);
- unilateral framework-contract changes require 2 months’ prior notice on a durable medium, with tacit acceptance and a free right to terminate during the notice period (DSP2 / CMF L314-13); every price-rise-via-new-offer path ([UC-OFF-06], [UC-SUB-17], [UC-MIG-03a]) must run through this 2-month mechanic;
- customer rejection/termination paths when a unilateral change requires them;
- legally mandatory pro-rata reimbursement of prepaid regular charges where applicable;
- operational separation between commercial boundary-effective cancellation ([UC-SUB-23]) and non-waivable legal termination/refund rights ([UC-BIL-09]).
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:
- daily sanctions / asset-freeze screening; a hit freezes the relationship and routes to compliance for closure or release;
- periodic KYC/KYB refresh; stale data triggers remediation and may restrict account actions until resolved;
- PEP/adverse-media and fraud monitoring;
- legal-capacity, representative-authority, death/incapacity, and country/licence perimeter changes;
- tax/regulatory residence data needed for FATCA/CRS or partner reporting;
- partner product continuing eligibility where the partner/regulation requires it.
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:
- a customer payment account is used for payment transactions;
- funds received for payment services are not deposits, savings, or e-money issued by Green-Got;
- idle balances must stay linked to a payment-service purpose. Topups, cashback credits, and long-idle positive balances blur the line between “funds held to execute payment transactions” (PI-legal) and deposit-/e-money-taking (not PI-legal) — the reason several neobanks moved to EME/CI licences. ⚠️ A stance on long-dormant positive balances is needed (e.g. nudges to spend/withdraw, or a defined treatment); balances must not become de-facto savings ([UC-SUB-25]);
- livret/ASV/PER remain intermediated partner products, not Green-Got deposit-taking;
- pending onboarding top-ups are not customer payment-account balances until KYC/account provisioning completes ([UC-ONB-20]);
- received payment-service funds are never commingled and are safeguarded by D+1 — full mechanics (in-transit receivables, fronting, shortfall remediation) in [UC-REG-09].
UC-REG-05 — Operational account lifecycle hooks
The commercial domain must expose states/references for account operations that are not purely commercial subscriptions:
- dormant/inactive accounts and reachability;
- deceased or incapacitated customers and representatives/heirs;
- legal seizures, garnishments, court orders, regulatory freezes;
- customer complaints and mediation/regulatory complaint references;
- post-closure access to statements and regulatory records;
- statement/archive access after migration or platform split ([UC-MIG-03o]).
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):
- Livret / banking savings → IOBSP (intermédiaire en opérations de banque et services de paiement, ORIAS-registered) — banking-intermediation conduct rules;
- ASV / PER (insurance-based) → IAS (intermédiaire en assurance, ORIAS) plus DDA/IDD conduct — devoir de conseil, suitability/adequacy assessment, and pre-contractual IPID / DIC (KID) delivery.
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”:
- Ledger & transactions — permanent. Financial integrity requires the ledger be reconstructable from the first ever transaction; every transaction is retained, linked to its account and holder. For a migrated customer this holds natively (the first GG transaction is the inbound money-move; PPS history is not booked into our ledger but kept in a separate read-only legacy ledger — see [UC-MIG-03n]).
- Invoices — permanent. Issued invoices are kept forever with their legally-required named party; the invoiced person’s identity cannot be stripped without destroying the record, so invoices are not anonymised.
- Holder profile PII — anonymisable after 10 years. A holder’s operational/profile PII (onboarding docs, contact, behavioural data) may be anonymised 10 years after the relationship ends, detaching identity from the financial records that don’t legally require the name.
- AML floor. Minimum 5-year retention of AML/KYC records after relationship end / operation (CMF L561-12).
- Legal-hold exception — keep forever. If the holder was under investigation and a suspicious activity was detected and declared (déclaration de soupçon to Tracfin), all their data is kept indefinitely with no anonymisation.
- GDPR storage-limitation / erasure (DSAR) is honoured only to the extent no overriding legal-retention obligation or hold applies. Failed/dropped onboardings ([UC-ONB-15]) and former customers ([UC-ONB-16]) follow this model.
⚠️ 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:
- acquirer in-transit receivable — money a customer paid via an acquirer (e.g. Stripe) not yet remitted to Green-Got: a receivable, not yet “funds received”, not safeguardable until received;
- received / suspense funds — landed in Green-Got’s collection/settlement account, incl. unallocated onboarding top-ups in a suspense ledger ([UC-ONB-20]); these are PSU funds and must be safeguarded by D+1;
- safeguarded customer positions — allocated to customer payment-account sub-ledgers.
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:
- inactivity detection and customer notifications at the legal cadence;
- after the statutory inactivity period, transfer of balances to the Caisse des Dépôts (CDC), with customer/heir search via Ciclade;
- post-transfer access: statements and records remain accessible ([UC-REG-05]); the relationship/account state reflects the CDC transfer so billing/subscription is wound down, not stranded.
⚠️ 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.)
- Vocabulary: Offer (commercial SKU, priced) composes Modules (atomic units); a Subscription links a holder to an offer; onboarding requirements are Steps. “Product” is not used.
- The catalog crates —
offers,subscriptions,customer_billing,accounting— are segment-neutral data + logic (no imports of segment systems). Onboarding is one consolidated cross-segment orchestrator ([UC-ONB-13]): all Steps (retail + B2B) live in a single registry gated byapplies_to, and it does depend on the domains it provisions into (organisation,core_banking,physical_person,investment). The existingbusiness_domain/onboardingscaffolding moves here rather than being split. - An onboarding is owned by an anonymous prospect token OR an authenticated user ([UC-ONB-28]): it can start from the logged-out “open an account” surface (anonymous, empty DataBag) or inside the app (authenticated, pre-filled). One-in-flight and ownership/authorization are per owner; at validation an anonymous prospect is promoted to a newly created user ([UC-ONB-29]), and a prospect who signs in mid-flow claims their session if they have none in flight ([UC-ONB-30]). The same Step registry/resolver serve both; only identity binding and pre-fill differ.
- Offer change = close-then-open. The contractual basis (price, module composition, entitlements) is immutable/versioned — never edited in place; only non-material presentation fields are editable in place ([UC-OFF-06]). Upgrade immediate (delta, daily pro-rata on gross); downgrade keeps perks to period end; commercial cancellation is boundary-effective by default, while legally required refunds/corrections remain modelled.
- Commercial eligibility is continuous ([UC-OFF-04]): re-evaluated at every subscription period; if a condition lapses at a period boundary the subscription transitions to its declared fallback ([UC-OFF-11]) — perks kept to the boundary, never a silent mutation, never a commercial refund. Deterministic boundaries (age) are the schedulable/notified sub-case; conditional discounts exist only inside a bundle. Ongoing compliance gates are a separate, continuously/periodically rechecked regime ([UC-REG-02]). Price snapshotted; promos intrinsic to the offer (no runtime stacking) → no cross-generation conflicts.
- Savings (livret/ASV/PER) are intermediated, free, commission-funded; reference the
investmentcrate; no customer billing. - Tier (Essential/Premium) lives on the Module; the account has no tier.
- Billing/recognition/payment are three distinct concerns. Subscription revenue is recognised over the service period (annual prepaid fees create PCA / deferred revenue), while billing lines carry the recognition period needed by accounting.
- Discounts, invoice-reducing commercial gestures, service-not-rendered credits, and billing-error corrections are contra-revenue by default. Cashback, bonuses, rewards, and pure compensation are classified separately through mandatory reason codes.
customer_billingowns customer invoices, credit notes, receivables, refund/customer-credit liabilities, tax-code snapshots, and accounting-source data. Chart-of-accounts mapping and double-entry journal generation live in a separate accounting integration layer.- VAT is tax-code driven and effective-dated: core payment services sit in the art. 261 C exemption perimeter, Green-Got’s subscription is currently taxable at 20% under the art. 260 B option, non-banking service fees are taxable by default, and partner commissions on ASV/PER/livrets are VAT-exempt intermediation revenue. TCA, where relevant, is borne by the insurer/partner, not by Green-Got.
- Partner-distributed ASV/PER/livrets produce partner commission revenue, not customer fees; the commission is a distinct revenue line and may be upfront or recurring on outstanding assets depending on the product/partner contract.
- Payment accounts are Green-Got-managed customer sub-ledger positions over safeguarded Crédit Mutuel liquidity; they are not deposits/savings/e-money issued by Green-Got. Green-Got also has a separate functioning/operating account (fees, marketing credits) and a collection/settlement account through which incoming funds transit. Received funds are non-commingled and safeguarded by D+1 (CMF L522-17); acquirer in-transit is a receivable, and any fronting gap is topped up from own funds ([UC-REG-09]).
- Data retention is purpose-based ([UC-REG-08]): ledger/transactions and invoices kept permanently (with their legally-named party), holder profile PII anonymisable after 10 years, AML 5-year floor, and forever-keep under a declared-suspicion legal hold.
- “Annual” is a conditional discount, not a no-exit lock: the bare payment-account framework contract is always resignable at will (CMF L.314-13); early exit forfeits the discount, not access or money ([UC-SUB-09]). Withdrawal/renunciation ([UC-BIL-14]) and partner commission clawback ([UC-ACC-15]) are non-waivable and fire commonly in the first period.
- Offers carry a visibility flag (
publicly_accessible, default true); restricted offers surface only to whitelisted emails/phones. Closed-beta / invite-only offers are the canonical restricted offers — restriction is about visibility, independent of price (employee/ambassador free is a subscription modifier, [UC-ONB-09], not a restricted price-0 offer). - Offers have a start date and optional end date; after the end date they are retired (not subscribable). Any offer with a condition (eligibility / continuing) needs a fallback for when the condition no longer holds; every non-primitive force-migratable offer must resolve to an active fallback, verified continuously; primitive offers (e.g. Essential) need none.
- Stale pre-fill data (expired id, KYC refresh due) is excluded from the DataBag so onboarding refreshes it.
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 frombusiness_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
- 1. Accounting Overview — the GL-view scope, the explicit contrast with
accounting_export, and the [UC-ACC-13] source-data-vs-GL-mapping boundary.
Model
- 2. Data Model — recognition entries, deferred revenue, accrued income, commission bookings, clawback provisions; the ER diagram and invariants.
Behaviour
- 3. Revenue Recognition and Deferral — recognise over the service period, PCA, one-off at event date, accrued income, period close ([UC-ACC-01..04]/[UC-ACC-12]).
- 4. Partner Commission and Clawback — commission revenue, the clawback schedule reference data, reversal events, the expected-clawback provision and its true-up ([UC-ACC-07]/[UC-ACC-14..17]).
- 5. VAT and Contra-Revenue — tax-code-driven VAT, the 260 B option, credit-note VAT recovery, discounts and reason-coded gestures ([UC-ACC-05]/[UC-ACC-06]/[UC-ACC-08]/[UC-ACC-09]).
- 6. Reconciliation and Billing Source — write-off loss, receivables↔GL reconciliation, and the source-data contract ([UC-ACC-10]/[UC-ACC-11]/[UC-ACC-13]).
Architecture
- 7. Architecture — the intended DDD layers, segment-neutrality, how the crate
consumes
customer_billingsource data, and where the GL-mapping layer begins.
Other
- Uncertainties — the active, classified register of open accounting items (per-product commission trigger/accrual basis, the 260 B validation + tax-code table, the reason-code taxonomy, GL-mapping-layer ownership).
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:
- GL (general ledger): the accounting view of Green-Got’s own revenue, expenses, assets and liabilities. This crate produces the revenue-side facts feeding it.
- Recognition: matching revenue to the period in which it is earned, independent of when it is billed or collected ([UC-ACC-01]).
- Recognition period: the
[period_start, period_end]over which a billing line is earned; carried on the billing source data, authoritative here ([UC-ACC-12]). - Deferred revenue (PCA — produits constatés d’avance): billed-but-not-yet-earned revenue, a liability that releases as the period elapses ([UC-ACC-01]/[UC-ACC-02]).
- Accrued income (FAE — facture à établir): earned-but-not-yet-billed revenue, an asset ([UC-ACC-04]).
- Contra-revenue: a revenue reduction (discount, reason-coded gesture, credit note), not an expense ([UC-ACC-05]/[UC-ACC-06]).
- Partner commission: revenue paid by a partner for distributing savings (ASV/PER/livret); there is no customer invoice ([UC-ACC-07]).
- Clawback (reprise): a partner’s recovery of upfront commission on renunciation / early surrender ([UC-ACC-15]).
- Write-off (créance irrécouvrable): an uncollectable receivable recognised as a loss, with the original revenue kept ([UC-ACC-10]).
- Tax code: a dated identifier carried per product/fee that resolves to a VAT treatment + rate via an effective-dated policy ([UC-ACC-08]).
- Integer cents (i64): every monetary amount is integer cents, never floating point — consistent
with
customer_billingand the rest of the platform.
2. This crate vs accounting_export
These two crates are easy to confuse and must never share data. The distinction is whose books:
| Concern | accounting (this crate) | business_domain/accounting_export |
|---|---|---|
| Whose books | Green-Got’s own revenue / P&L | A B2B customer’s own books |
| Audience | Green-Got finance + Green-Got’s GL | The customer’s accountant / tooling |
| Output | Recognition, deferral, commission, VAT, reconciliation facts | FEC, Pennylane, journal export of the customer’s transactions |
| Source | The customer_billing subledger (Green-Got’s billing of customers) | The customer’s banking transactions + their invoices |
| Relation | Green-Got is the seller recognising its own revenue | Green-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:
- Recognition schedules per billing line — spread over the recognition period ([UC-ACC-01]), one-off at the event date ([UC-ACC-03]), or accrued when billed in arrears ([UC-ACC-04]).
- Deferred revenue (PCA) and accrued income (FAE) projections rolled forward each period ([UC-ACC-02]/[UC-ACC-12]).
- Contra-revenue from discounts and reason-coded commercial gestures ([UC-ACC-05]/[UC-ACC-06]).
- Partner-commission revenue, clawback events, expected-clawback provisions, and their true-up ([UC-ACC-07]/[UC-ACC-14..17]).
- VAT determination from the snapshotted tax code + dated policy, and VAT recovery on credit notes ([UC-ACC-08]/[UC-ACC-09]).
- Write-off loss recognition and the receivables↔GL reconciliation ([UC-ACC-10]/[UC-ACC-11]).
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:
- the chart-of-accounts mapping (which GL account each fact posts to), or
- the double-entry journal generation (the balanced debit/credit postings).
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
- Invariant: Revenue is recognised by recognition period, never by billing or collection date ([UC-ACC-01]/[UC-ACC-03]/[UC-ACC-04]).
- Invariant: No row in this crate names a GL account or asserts a double-entry posting; facts are account-neutral ([UC-ACC-13]).
- Invariant: This crate shares no tables with
accounting_export. - Invariant: All monetary amounts are integer cents (i64); no floating-point money.
- Invariant: Σ recognised + deferred (PCA) + accrued (FAE) reconciles to the gross of the source billing lines for any period cut ([UC-ACC-12]).
6. Related documents
- 2. Data Model — the entities the above runs over.
- 6. Reconciliation and Billing Source — the source-data contract and the [UC-ACC-13] boundary in detail.
customer_billing— the subledger producing the source data.business_domain/accounting_export— the other accounting crate (customer’s books), explicitly not this one.
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
- Account-neutral facts. No entity here carries a GL account number or a debit/credit pair; that is the downstream GL-mapping layer ([UC-ACC-13]).
- Source references, not copies. Each fact references the originating
customer_billingobject (billing line, credit note, write-off) or partner event by id; it copies only the snapshotted values it needs to be auditable (gross, tax code, reason code, recognition period). - Derived balances are computed, never stored redundantly. Deferred revenue (PCA), accrued income (FAE), revenue-by-period, and the receivables↔GL balance are read models over the entries below, not authoritative columns that could drift ([UC-ACC-12]).
- Integer cents. Every money field is
i64cents; no floating point.
2. Entities
2.1 RecognitionEntry
The core fact: one recognition schedule per billing line, carrying how its gross is earned over time.
| Field | Notes |
|---|---|
id | rec_… typed id |
billing_line_ref | the customer_billing source line ([UC-ACC-13]) |
kind | over_period ([UC-ACC-01]) | one_off_at_event ([UC-ACC-03]) | accrued_in_arrears ([UC-ACC-04]) |
gross_cents | gross revenue of the line (before contra-revenue) |
period_start, period_end | the recognition period; for one-off, start = end = event_date ([UC-ACC-03]) |
tax_code, vat_cents | snapshotted tax code + resolved VAT ([UC-ACC-08]) |
legal_entity_ref, customer_ref | party references for reconciliation ([UC-ACC-11]) |
product_ref | product/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]):
- PCA at date d = Σ
gross − earned_to(d)overover_periodentries whose period extends past d. - FAE at date d = Σ
earned_to(d) − billedoveraccrued_in_arrearsentries not yet invoiced.
2.3 ContraRevenueEntry
A revenue reduction. Discounts are intrinsic; gestures are reason-code driven ([UC-ACC-05]/[UC-ACC-06]).
| Field | Notes |
|---|---|
id | typed id |
billing_line_ref? | the reduced line, when invoice-linked |
reason_code | mandatory; drives the treatment per the [UC-BIL-16] taxonomy |
treatment | contra_revenue | customer_credit_liability | expense ([UC-ACC-06]) |
amount_cents | reduction amount |
2.4 CommissionBooking
Partner-commission revenue for distributed savings — no customer invoice ([UC-ACC-07]).
| Field | Notes |
|---|---|
id | cmb_… typed id |
partner_ref, product_ref, policy_ref | who / what / which contract |
kind | upfront | recurring_on_assets ([UC-ACC-07]) |
gross_cents | commission earned from the partner event/statement |
provision_cents | expected-clawback provision raised at booking ([UC-ACC-16]); 0 for recurring |
net_cents | gross − provision — the revenue actually recognised net of provision |
basis, recognised_at | the 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]).
| Field | Notes |
|---|---|
renunciation_window_days | within 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_basis | what the reversal % applies to |
settlement_mode | netting (default) | invoice |
per_transfer_triggers_clawback | PER 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]).
| Field | Notes |
|---|---|
id | clw_… typed id |
policy_ref, originating_commission_booking_id | links back to what is being reversed |
trigger | policy_renounced | policy_surrendered_early | transfer_out |
amount_cents | the reversal; draws down the provision if one exists ([UC-ACC-16]) |
period | the 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]).
| Field | Notes |
|---|---|
id | prv_… typed id |
partner_ref, product_ref, cohort | the true-up grouping |
expected_rate | from observed renunciation/surrender curves, versioned |
held_cents | the liability currently held |
version | the curve version the rate came from |
2.8 VatPolicy (effective-dated) and WriteOff
VatPolicy— resolves atax_codeto{treatment, rate, statutory_ref}for a date range (effective_from/effective_to?); the policy can change (e.g. if Green-Got renounces the 260 B option) without rewriting history ([UC-ACC-08]).WriteOff—{invoice_ref, loss_cents, at}: recognises a loss; the original revenue is kept, not reversed ([UC-ACC-10]).
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
- Invariant: Every
RecognitionEntryreferences acustomer_billingsource line; this crate never originates the billing line ([UC-ACC-13]). - Invariant: For a
one_off_at_evententry,period_start = period_end = event_date([UC-ACC-03]). - Invariant: Deferred revenue (PCA) and accrued income (FAE) are derived projections over
RecognitionEntry, never standalone authoritative columns ([UC-ACC-02]/[UC-ACC-12]). - Invariant: Every
ContraRevenueEntrycarries a mandatoryreason_code; thetreatmentis the [UC-BIL-16] mapping of that code, never set independently ([UC-ACC-06]). - Invariant:
CommissionBooking.net_cents = gross_cents − provision_cents([UC-ACC-16]); arecurring_on_assetsbooking hasprovision_cents = 0. - Invariant: A
ClawbackSchedule.reprise_periods[]is ordered, contiguous and non-overlapping, and its final tier is0%([UC-ACC-14]). - Invariant: A
ClawbackEventreferences itsoriginating_commission_booking_id; undernetting, the period’s events sum to the partner statement’s negative line ([UC-ACC-15]). - Invariant: A
WriteOffrecognises a loss and never reduces the original recognised revenue ([UC-ACC-10]). - Invariant: VAT is resolved from the snapshotted
tax_code+ the datedVatPolicy, not hardcoded by the catalogue ([UC-ACC-08]). - Invariant: All money is integer cents (i64); no floating-point.
5. Related documents
- 3. Revenue Recognition and Deferral — how
RecognitionEntryearns out. - 4. Partner Commission and Clawback — the commission/clawback entities in motion.
- 5. VAT and Contra-Revenue —
VatPolicyandContraRevenueEntrybehaviour. - 6. Reconciliation and Billing Source —
WriteOff, reconciliation, and the source-data contract.
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
kind | When recognised | Source case | UC |
|---|---|---|---|
over_period | Prorata temporis across [period_start, period_end] | recurring subscription billed in advance | [UC-ACC-01] |
one_off_at_event | Fully at event_date (period_start = period_end) | ATM / FX / reissue fee | [UC-ACC-03] |
accrued_in_arrears | Earned as service is rendered, before the invoice exists | service 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 month | Recognised (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
- Invariant: Earned-to-date is a pure function of
kind, the recognition period and the cut date; it does not depend on billing or collection dates ([UC-ACC-01]/[UC-ACC-03]). - Invariant: For an
over_periodentry,recognised + deferred = grossat every cut date ([UC-ACC-02]). - Invariant: For an
accrued_in_arrearsentry, the accrual lands in the recognition-period month, not the invoice month ([UC-ACC-04]). - Invariant: A
one_off_at_evententry hasperiod_start = period_end = event_dateand is never deferred ([UC-ACC-03]). - Invariant: Period close performs no manual cut-off for deferred/accrued lines — they are pre-positioned by the line-level recognition period ([UC-ACC-12]).
- Invariant: All recognition arithmetic is in integer cents.
8. Related documents
- 2. Data Model —
RecognitionEntryand the PCA/FAE projections. - 5. VAT and Contra-Revenue — how discounts/credit notes adjust the recognised gross.
- 6. Reconciliation and Billing Source — period close feeding the receivables↔GL reconciliation.
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:
renunciation_window_days— within this window, renunciation is a 100% reversal.reprise_periods[]— ordered, contiguous, non-overlapping{from_month, to_month, clawback_pct}tiers; the final tier0%marks the point past which commission is unconditionally earned.clawback_basis— what the reversal percentage applies to.settlement_mode—netting(default) |invoice.per_transfer_triggers_clawback— a PER flag: atransfer_outreverses the tierclawback_pctfor the exit month.
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:
| Trigger | Effect |
|---|---|
policy_renounced (within renunciation_window_days) | 100% reversal |
policy_surrendered_early | reverse 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:
- Compare cumulative actual clawbacks ([UC-ACC-15]) to the provision held.
- Re-estimate the rate from trailing actuals (a new versioned curve).
- Adjust the provision — an increase posts contra-revenue; a release posts revenue.
- 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
netting(default) — period clawback events sum to the partner statement’s negative line; the residual against the statement is the settlement variance ([UC-ACC-15]).invoice— clawback is settled by an explicit invoice flow rather than netting against new commission.
8. Invariants
- Invariant: A partner commission produces no
customer_billinginvoice; it is a distinct revenue line in this crate only ([UC-ACC-07]/[UC-BIL-15]). - Invariant: Upfront commission is recognised net of the expected-clawback provision;
net = gross − provision([UC-ACC-16]). - Invariant: A
ClawbackEventexists for every exit whatever the recognition policy, carries itsoriginating_commission_booking_id, and draws down the provision when one exists ([UC-ACC-15]). - Invariant: Clawback %, windows and tiers are reference data ([UC-ACC-14]); nothing is hardcoded.
- Invariant: Passing the final
0%tier releases the remaining provision — commission is then unconditionally earned ([UC-ACC-17]). - Invariant: Under
netting, the period’s clawback events reconcile to the partner statement’s negative line up to the settlement variance ([UC-ACC-15]).
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
- 2. Data Model §2.4–2.7 — the commission / clawback entities.
- 5. VAT and Contra-Revenue §3 — partner commission is VAT-exempt intermediation revenue.
investmentcrate + partners — the upstream savings contracts and commission events.
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)
| Revenue | Treatment | Notes |
|---|---|---|
| Core banking / payment services | Exempt | within the art. 261 C exemption perimeter |
| Green-Got subscription fee | Taxable @ 20% under the art. 260 B option | ⚠️ provisional — needs tax-advisor validation (see §1.2) |
| Non-banking service fees | Taxable by default | unless their own tax_code says otherwise |
| Partner commissions (ASV/PER/livrets) | Exempt | VAT-exempt intermediation revenue (see §3) |
| TCA (where relevant) | Borne by insurer/partner | ASV / 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 invoice | credit note reducing revenue |
| cashback / bonus / reward not linked to an invoice | customer-credit / pending-balance liability (not an invoice credit note) |
| pure compensation / indemnity not reducing a price | expense, with a customer liability until settled |
| billing error / double charge | correction 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
- Invariant: VAT is resolved from the snapshotted
tax_code+ the effective-datedVatPolicy+ territory at issuance; it is never hardcoded by the catalogue ([UC-ACC-08]). - Invariant: A later VAT-policy change never retroactively mutates an already-snapshotted line ([UC-ACC-08]).
- Invariant: A credit note reverses both the revenue and the snapshotted VAT of the original line, using that line’s treatment ([UC-ACC-09]).
- Invariant: Partner commission carries no output VAT — exempt intermediation revenue ([UC-ACC-08]).
- Invariant: Discounts are contra-revenue, never reclassified as expense within this model ([UC-ACC-05]).
- Invariant: Every
ContraRevenueEntrycarries a mandatoryreason_codewhose [UC-BIL-16] mapping is the sole source of itstreatment([UC-ACC-06]). - Invariant: The 260 B subscription treatment is provisional / configurable until tax-advisor validated; it is not a constant ([UC-ACC-08], launch-blocking).
6. Related documents
- 2. Data Model —
VatPolicyandContraRevenueEntry. - 3. Revenue Recognition and Deferral — the recognised gross these adjustments reduce.
- 4. Partner Commission and Clawback — exempt commission revenue.
invoicing10. Tax and VAT — the AR-side VAT computation model (separate domain; shared rate/category vocabulary).
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.
| Event | Revenue | Loss / VAT |
|---|---|---|
| Credit note ([UC-ACC-09]) | reversed | VAT recovered |
| Write-off ([UC-ACC-10]) | kept | loss 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)
- 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
- customer / legal-entity references
4.2 What this crate does not own
- the chart-of-accounts mapping — which GL account each fact posts to;
- the double-entry journal generation — the balanced debit/credit postings;
- GL account mapping and journal depth generally.
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
- Invariant: A write-off recognises a loss and keeps the original recognised revenue ([UC-ACC-10]).
- Invariant: At every cut date,
Σ open invoice balances = GL client-receivable balance; a mismatch is an anomaly, never auto-reconciled ([UC-ACC-11]). - Invariant: This crate consumes only the §4.1 source fields; it never originates billing lines, invoices, credit notes or payments ([UC-ACC-13]).
- Invariant: No fact emitted here names a GL account or asserts a double-entry posting ([UC-ACC-13]).
6. Related documents
- 1. Accounting Overview §4 — the boundary stated at the scope level.
- 2. Data Model §2.8 —
WriteOff. - 3. Revenue Recognition and Deferral §6 — period close.
- 7. Architecture §4 — the boundary in DDD terms.
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.
| Layer | Contents | Notes |
|---|---|---|
domain/ | recognition, deferral, contra-revenue, commission, clawback, VAT, reconciliation value objects + invariants | zero cross-layer deps; no GL-mapping knowledge ([UC-ACC-13]) |
use_cases/ | recognise line, close period, book/claw commission, true-up, determine VAT, reconcile | pure 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 calls | the 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]
on_billing_line_created→recognise_billing_linebuilds theRecognitionEntryschedule.on_credit_note→reverse_on_credit_notereverses revenue + VAT ([UC-ACC-09]).on_write_off→write_off_receivablerecognises the loss, keeps revenue ([UC-ACC-10]).on_commission_event→book_commission/apply_clawback([UC-ACC-07]/[UC-ACC-15]).
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.
- In this crate: recognition / deferral / accrual schedules, commission + clawback + provision, VAT determination, write-off loss, receivables↔GL reconciliation — all expressed as facts (amount, kind, period, tax, party, reason).
- Not in this crate: the chart-of-accounts mapping (which GL account) and the double-entry journal generation (balanced debit/credit postings) — a separate accounting integration layer fed by these facts ([UC-ACC-13]).
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
| Concern | Owner |
|---|---|
| Billing lines, invoices, credit notes, refunds, customer-credit liabilities, tax-code snapshots, reason codes, recognition periods | customer_billing |
| Savings / ASV / PER / livret contracts + partner-commission events | investment + partners |
| Chart-of-accounts mapping + double-entry journal | separate 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
- Invariant:
domain/has zero cross-layer dependencies and no GL-mapping knowledge ([UC-ACC-13]). - Invariant: The crate imports no segment systems; segment is a data field, not a dependency.
- Invariant: Recognition is event-driven off
customer_billing/ partner events viarules/; the crate never reads another domain’s tables directly. - Invariant: No emitted fact names a GL account or asserts a double-entry posting ([UC-ACC-13]).
7. Related documents
plan.md— the module layout + intended schema.- 1. Accounting Overview — scope and the boundary at product level.
- 6. Reconciliation and Billing Source §4 — the source-data contract.
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.
- Class:
legal/tax· Owner: Tax advisor → Finance / Backend · Due: Before GL go-live · Launch-blocking: yes - Evidence: 5. VAT and Contra-Revenue §1, spine UC-ACC-08
- Status: VALIDATION PENDING (tax advisor)
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.
- Class:
product-decision/partner-contract· Owner: Claude (research) → Finance / Partnerships · Due: Before savings GL go-live · Launch-blocking: yes (for savings revenue) - Evidence: 4. Partner Commission and Clawback §1–2, spine UC-ACC-07 / UC-ACC-14
- Status: RESEARCH PENDING (Claude)
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.
- Class:
product-decision· Owner: Claude (research) → Finance / Backend · Due: Before GL go-live · Launch-blocking: no (model is correct; the enum needs finalising) - Evidence: 5. VAT and Contra-Revenue §4.2, spine UC-ACC-06 / UC-BIL-16
- Status: RESEARCH PENDING (Claude)
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.
- Class:
product-decision/architecture· Owner: Finance / Backend · Launch-blocking: no (this crate is correct regardless; the consumer can land later) - Port to: 7. Architecture §4 + 6. Reconciliation and Billing Source §4 once the owner is named.
- Status: BOUNDARY RESOLVED; owner OPEN.
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
- 5. VAT and Contra-Revenue — VAT baseline, 260 B option, reason-coded contra-revenue.
- 4. Partner Commission and Clawback — commission trigger / accrual basis.
- 7. Architecture — the [UC-ACC-13] boundary the GL-mapping-layer ownership item sits behind.
customer_billing— the [UC-BIL-16] reason-code source the taxonomy is shared with.
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.aidomains,ubble.aiIDs, and the Ubble dashboard. Docs use both names interchangeably; HTTP headers are prefixedCko-(Checkout.com). We call our integration “checkout” internally (matches the legacy@green-got/checkoutpackage) 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.
| Layer | Location | Owns |
|---|---|---|
| Transport client | src/clients/src/checkout/ | mTLS + Basic-auth HTTP client, v2 request/response DTOs, Cko-Signature verification primitive. Pure I/O. Mirrors the yousign submodule. |
| Domain crate | src/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. |
| Migrations | src/db/migrations/2026…_checkout_pvid.{up,down}.sql | The local mirror tables. |
| Back-office API | src/services/back_office_api/src/routes/ | #[handler(domain = "crm")] read/manage endpoints under /crm/checkout_id_verification. |
| Parameters | src/env/src/parameters/checkout.rs | mTLS 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.:
start(subject) -> { verification_id, redirect_url }→ step returns Pending and surfaces theredirect_urlto the app; the bag stores theverification_idand a status ofPending.- The webhook (handled in
checkout_pvid) drives the verification to a terminal state and writes back the onboardingIdentityVerificationStatus(Verified / Failed), letting the resolver advance. (Mirrors the legacy design: webhook → fetch full IDV → update KYC/onboarding → signal waiting workflows.)
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:
- 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
mtlscrate (MtlsClientConfig+HttpMtlstrait) to build thereqwest::Client. - HTTP Basic auth —
client_id/client_secretin theAuthorizationheader, per environment.
✅ Status: the mTLS API certificate (cert + key) and the webhook signature public keys are now encrypted with
gg env encryptand pushed intoparameters/checkout.rs— test identity fordevelopment/staging, production identity forproduction. Theenvcrate compiles and round-trips.✅ mTLS alone authenticates (verified 2026-06-30). A live probe with the test client certificate proved it:
GET /v2/applicantsreturns 401 with no client cert but 405 (method-not-allowed — past the auth gate) when the test cert is presented. So the HTTP Basicclient_id/client_secretthe docs mention are not required for our account; they remainSecret::todo()and the client sends them anyway (empty, harmless).✅
user_journey_idprovisioned (2026-06-30): the “Green-Got” journeyusj_01h13smkbjh2x48zn7v6mkp8g0is 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:
- Create / reuse applicant —
POST /v2/applicants→applicant_id(aplt_…). One applicant per person, reused across verifications. We store theapplicant_idagainst the user/onboarding owner. In non-prod, magicexternal_applicant_idvalues drive deterministic outcomes (§9). - Create identity verification —
POST /v2/identity-verificationswithapplicant_id,declared_data {name, birth_date?},webhook_url,user_journey_id(= our Certified/PVID config). Returnsidv_…, statuscreated. Declared name should be the legal name if the user has updated legal data (Ubble matches the captured document against the declared name). - Create attempt —
POST /v2/identity-verifications/{idv}/attemptswithredirect_url(+ optionalphone_number,client_informationto pre-select country/doc-type/language). Returnsiatp_…and_links.verification_url.href, statuspending. The verification URL expires after 15 minutes — create a fresh attempt each time the user (re)enters the flow. - User captures at the hosted web app
https://id.ubble.ai/<token>/(or embedded via the iframe/webview SDK), then is sent to ourredirect_url. - Ubble processes (status
checks_in_progress) and fires webhooks at each step. - On completion (
identity_verification_checks_completed): we callGET /v2/identity-verifications/{idv}to fetch the full extracted data, and…/attempts/{iatp}/assetsfor the captured media URLs (which we must download promptly — they expire in ~1h).identity_verification_report_createdsignals 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.
| Method | Path | Purpose |
|---|---|---|
| POST | /v2/applicants | Create applicant (external_applicant_id?, external_applicant_name?, email?). |
| GET | /v2/applicants/{id} | Retrieve applicant. |
| POST | /v2/applicants/{id}/anonymize | GDPR erase. |
| POST | /v2/identity-verifications | Create verification. |
| GET | /v2/identity-verifications/{id} | Retrieve verification + all extracted results (main read). |
| POST | /v2/identity-verifications/{id}/attempts | Create attempt / retry. |
| GET | /v2/identity-verifications/{id}/attempts | List attempts (paginated). |
| GET | /v2/identity-verifications/{id}/attempts/{aid} | Retrieve one attempt. |
| GET | /v2/identity-verifications/{id}/attempts/{aid}/assets | Captured media pre-signed URLs (~1h TTL). |
| GET | /v2/identity-verifications/{id}/pdf-report | { "pdf_report": "<url>" }. |
| POST | /v2/identity-verifications/{id}/notify | Re-fire the webhook for the current state (testing/integration). |
| POST | /v2/identity-verifications/{id}/anonymize | GDPR 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)
- Header:
Cko-Signature: <timestamp>:<org>-<env>-<version>:<signature>e.g.1635236316.377888:637-prod-v1:5257a869…. - Algorithm: ECDSA with SHA-512 (matches the P-521 signature keys we hold —
secp521r1+ SHA-512 is the standard pairing). The legacy TS code usedSHA512withECDSA. - Procedure:
- Parse the header →
timestamp,key_id,signature. - Select the public key by matching the literal
key_idmiddle segment of the header (do not hardcode the env spelling — docs examples showproduction/livewhile our keys areprod/test). Assert org=637and the expected version. - Reconstruct the signed payload by combining the raw request body with the
Cko-Signaturetimestamp (legacy used"<timestamp>:<rawBody>"). - ECDSA-SHA512-verify against the selected public key.
- Reject if the timestamp is older than a small window (legacy: 5 minutes) to block replay.
- Parse the header →
- ⚠️ Confirm exact byte ordering/encoding (body+timestamp vs timestamp+body,
separator, hex vs base64url signature) against Ubble’s
code-samplesrepo and a captured real webhook before trusting it. This is the one genuinely under-specified spot. See §12.
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 :8080 → http://localhost:8080/checkout/webhook.
We use Tailscale Funnel (we already run Tailscale) to expose it publicly:
-
On macOS the CLI ships inside the app bundle — alias it once (on Linux
tailscaleis already onPATH):alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
-
Find your node’s MagicDNS name (the public host) in the
Selfrow oftailscale status— e.g.your-machine.tailnet-XXXX.ts.net. -
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:8080listener, 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 — seeinternal_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 prefertailscale funnel --bg offas soon as you’re done. -
Your webhook URL is then:
https://<node>.<tailnet>.ts.net/checkout/webhook
Pass it as
webhook_urlon create-verification; re-fire events on demand withPOST /v2/identity-verifications/{id}/notify(§6). -
Confirm with
tailscale funnel status(should show:443 → 127.0.0.1:8080).
Prerequisites / gotchas:
- Funnel must be enabled on the tailnet — the
funnelnode attribute in the ACL policy. If it isn’t,tailscale funnelprints an admin-console link and a tailnet admin must grant it. - The backend must be running on
:8080for anything to answer. - Signatures: we hold only the public signature key, so a hand-forged
curlwebhook fails verification (401) — genuinely signed webhooks must come from Ubble through the funnel. For pure handler testing without a valid signature, add a temporary dev-only bypass of theCko-Signaturecheck. This is also when to confirm the byte-ordering question above. - Quick reachability check before wiring the backend:
python3 -m http.server 8080+ funnel, thencurl https://<node>.<tailnet>.ts.net/. - Alternatives:
ngrok http 8080orcloudflared tunnel --url http://localhost:8080give an equivalent public URL if Funnel isn’t available.
8. Statuses & lifecycle
Verification status (status on the IDV resource):
created → pending → capture_in_progress → checks_in_progress → terminal:
approved / declined / retry_required / refused / inconclusive.
| Status | Meaning | Terminal? |
|---|---|---|
created | Verification created, no attempt yet. | no |
pending | Attempt exists, verification_url available. | no |
capture_in_progress | Applicant is capturing. | no |
checks_in_progress | Capture done, checks running. | no |
approved | Verified OK. | yes |
declined | Irregularity/fraud found. Retry ⇒ new verification. | yes |
retry_required | Couldn’t complete checks. Retry ⇒ new attempt on same idv. | recoverable |
refused | Applicant explicitly refused. | yes |
inconclusive | Checks 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):
capture_completed→checks_in_progresschecks_completed→approvedordeclinedcapture_refused→refusedcapture_aborted→retry_requiredchecks_inconclusive→retry_required
Map to onboarding IdentityOutcome: approved ⇒ Verified; declined/refused/
inconclusive ⇒ Failed; 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):
- Top-level:
id(idv_…),created_on,modified_on,user_journey_id,applicant_id,status,response_codes[] {code, summary},risk_labels[](currently onlymultiple_faces_detected),declared_data {name, birth_date?},verification_policy_version(Certified/PVID only — persist for audit),phone_number,webhook_url,redirect_url,_links. verified_identity(reconciled person):first_names,full_name,last_name,last_name_at_birth,birth_date,birth_place,nationality,gender(M/F/NA).documents[](one per captured doc): identity fields (full_name,first_names,last_name,last_name_at_birth,birth_date,birth_place,nationalityISO-3166-1 alpha-2,gender); document meta (document_type∈ Driving licence / ID / Other / Passport / Residence Permit / Visa,document_number,document_issuing_country,document_issue_date,document_issue_place,document_expiry_date,document_mrzraw MRZ);personal_number,tax_identification_number; residence-permit fields (permit_obtaining_date,permit_expiry_date,permit_type_detailed,permit_type_remarks); image signed URLs (front_image_signed_url,back_image_signed_url,signature_image_signed_url, ~1h TTL).face:{ image_signed_url }.
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
type ∈ face_image, face_video, document_front_image/_video,
document_back_image/_video, document_signature_image (+ secondary_document_*).
No numeric scores. v2 exposes a decision (
status) + categoricalresponse_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:
10000—approved.61xxx—retry_required(recoverable): engagement (61101never started,61112no document,61113camera refused,61121drop), streaming (61201/61202/61203), doc-capture quality (61301blurry,6131xnot captured,61314document_challenge_timeout — certified), face-capture (61410not captured,61412face_not_turned/liveness,61413face_challenge_timeout — certified),61901internal_error.62xxx—declined(terminal, fraud/non-compliance):62101expired,62102not accepted,62103damaged,62201photocopy,62202screenshot,62301counterfeit,62304/62321face_mismatch, … (pull the full 623xx/624xx table from the OpenAPI spec before finalizing the enum).63xxx— refusal (e.g.63001).
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:
eaplt_10000000000000000000000000→ approved.eaplt_62XXX0000000000000000000000→ declined with code62XXX(e.g.eaplt_62201…= photocopy).eaplt_61XXX…variants → retry-after-aborted / retry-after-inconclusive.
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
Basic-auth credentials— RESOLVED (2026-06-30): a live probe with the test cert showed mTLS alone authenticates (401without cert →405with cert onGET /v2/applicants). Basicclient_id/client_secretare not required; left asSecret::todo().Cko-Signatureexact construction — confirm body+timestamp ordering, separator, and signature encoding (hex vs base64url) againstgithub.com/ubbleai/code-samplesand a captured webhook.- Signature key env token — our keys say
prod/test; docs examples sayproduction/live. Match on the literal header segment; confirm the real token Ubble sends. — RESOLVED (2026-06-30): the “Green-Got” journeyuser_journey_id(Certified/PVID config)usj_01h13smkbjh2x48zn7v6mkp8g0is provisioned and set in all environments. (If Ubble later issues a distinct live journey, updateproduction().)- 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.
- Onboarding seam shape — confirm the move from synchronous
verify()tostart+ webhook-driven completion (§3). - Key rotation — the production mTLS private key was shared over chat during this work; recommend rotating it before go-live.
13. Deliverables (build plan)
- ✅
src/env/src/parameters/checkout.rs— params +gg-pushed secrets; registered inparameters/mod.rs. Done for the mTLS cert/key, signature public keys, and API URL (signature_key_idset). Pending Ubble: Basicclient_id/client_secret(Secret::todo()) anduser_journey_id(empty). src/clients/src/checkout/— transport client (mTLS viamtlscrate, Basic auth, v2 DTOs, operations,Cko-Signatureverifier);pub mod checkout;inclients/src/lib.rs.- Datamodel + migrations —
checkout_pvidlocal mirror tables (applicant, verification, attempt, document, response_codes, assets) insrc/db/migrations/. src/commercial_domain/checkout_pvid/— domain crate: stores, use-cases (start / handle-webhook / refresh / archive-assets / back-office queries), status machine, and theIdentityVerificationProviderimpl wired into onboarding.- Back-office API —
#[handler(domain = "crm")]endpoints to list/inspect/manage verifications. - Back-office routes/views —
/crm/checkout_id_verificationunder/crm/validations: list attempts/successes with status + link to user / onboarding / KYC review. - Business-app test module — request an identity verification and observe the result end-to-end.
Sources
- OpenAPI v2 spec (authoritative):
https://raw.githubusercontent.com/ubbleai/docs/main/openapi.yaml - Rendered docs:
https://docs.ubble.ai/docs/introduction - Attempt statuses:
https://docs.ubble.ai/docs/reference-info/attempt-statuses - Dashboard / credentials:
https://dashboard.ubble.ai/ - Code samples (signature, SDK):
https://github.com/ubbleai/code-samples,https://github.com/ubbleai/integration_examples - Legacy Green-Got implementation:
back/packages/checkout(TS, mTLS + ECDSA-SHA512 webhook verify)
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
- 1. Customer Billing Overview — subledger scope; the explicit
contrast with the
invoicingcrate; the funds, savings, and accounting boundaries. - 2. Data Model — BillableEvent / Invoice / CreditNote / Payment / Refund / Statement; ER diagram; per-line recognition metadata; invariants mapped to UCs.
Billing principles
- 3. Billing Model — In Advance — recurring fees billed in advance at the anniversary; one-off fees at the event; collection by internal transfer (no SEPA DD).
- 4. Invoice Numbering & Immutability — gap-free numbering per Green-Got legal entity; immutable invoices; partial payment never mutates the invoice.
- 5. Partial Payment & Dunning — partial payment, carried shortfall oldest-first, sweep, the dunning cadence, write-off vs credit note.
- 6. Refunds & Reason Codes — the commercial-no/legal-yes posture, the reason-code taxonomy, online withdrawal / rétractation.
- 7. VAT & Tax Codes — VAT determined per line by a dated
tax_code; the 261 C exemption vs 260 B option. - 8. E-Invoicing & E-Reporting — e-invoicing-compatible build; the B2B transmission vs B2C e-reporting limbs.
Architecture
- 9. Architecture — the intended DDD layers; segment-neutral design; the accounting-source output contract.
Other
- Uncertainties — the active, classified register of open billing items (rétractation windows, reason-code taxonomy, promo/referral anti-abuse, cashback rates/tax, 260 B tax-code), each with owner, due, launch-blocking flag, and evidence.
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
- Subledger: the detailed customer-by-customer record of money obligations (billable events,
invoices, payments, credit notes, refunds) that rolls up into, but is distinct from, the general
ledger.
customer_billingis the subledger;accountingowns the GL (UC-ACC-13). - Green-Got legal entity (issuer): the Green-Got entity on whose behalf a customer invoice is issued — the unit the gap-free numbering attaches to (UC-BIL-12).
- Customer: the holder Green-Got bills for the services it sells them. The customer is the
buyer; Green-Got is the seller/issuer. (The opposite of the
invoicingcrate, where the Green-Got business customer is the seller.) - BillableEvent: a captured chargeable economic event (a subscription cycle opening, an ATM/FX/ reissue fee) with its exact recognition date — the pre-invoice fact (UC-BIL-04).
- Invoice: the immutable customer invoice, gap-free numbered per Green-Got legal entity (UC-BIL-12).
- CreditNote: a document that reduces what the customer owes (gesture, service-not-rendered, error correction), reason-coded (UC-BIL-07/10/16).
- Payment: settlement of a receivable by internal transfer; never creates revenue (UC-BIL-05).
- Refund: cash returned to the customer for a non-waivable legal / correction case (UC-BIL-09).
- Statement: a non-fiscal monthly aggregate of a customer’s billing activity (UC-BIL-11).
- Reason code: the mandatory classifier on every credit / refund / reward that drives accounting treatment (UC-BIL-16).
2. Scope — what customer_billing owns
This crate owns the full customer-facing money lifecycle for services Green-Got sells:
- Billable events — capture each chargeable event with its recognition date (UC-BIL-04).
- Invoices — issue an immutable, gap-free-numbered invoice for every charge, subscription fees and one-off fees alike; subscriptions billed in advance (UC-BIL-01/02/12). A €0 offer produces no invoice at all, but one-off fees on it still bill (UC-BIL-13).
- Collection — request the internal transfer that settles a receivable, supporting partial payment and carried shortfall (UC-BIL-05/24).
- Credit notes — reduce a customer’s obligation (gesture, service-not-rendered, error correction), reason-coded (UC-BIL-07/10/16).
- Refunds — the non-waivable legal / correction cash-back path (UC-BIL-09/14/21).
- Statements — the non-fiscal monthly aggregate (UC-BIL-11).
- Recognition metadata + VAT — each line carries a recognition period and VAT resolved from a
dated
tax_code(UC-BIL-23 / UC-ACC-08). - Accounting-source data — the gross/discount/net, VAT, recognition period, receivable/payment
state, reason code, and references exported to
accounting(UC-ACC-13).
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.
| Concern | customer_billing (this crate) | business_domain/invoicing |
|---|---|---|
| Who is the issuer/seller | Green-Got itself | The business customer |
| Who is the buyer | The Green-Got customer (holder) | The business customer’s own client |
| What it bills | Green-Got’s services (subscriptions, fees) | The customer’s goods/services to third parties |
| Gap-free series | Green-Got’s own series, per Green-Got legal entity | The business customer’s series, per their org |
| E-invoicing transmission | E-reporting / B2B transmission per 8 | Transmitted via plateforme_agreee (B2Brouter) |
| Nature | A private subledger | Accounts 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
| Owns | References (owned elsewhere) |
|---|---|
| Billable events, invoices, credit notes, payments, refunds, statements | The banking account / IBAN / balance / safeguarded liquidity (core_banking) |
| Per-line recognition metadata; tax-code snapshots | Subscription lifecycle, free-month counter, proration instructions (subscriptions) |
| Accounting-source data | Chart-of-accounts mapping; double-entry journals (accounting) |
| Receivable / payment / refund-liability state | Payment execution, disputes, chargebacks (payments domain) |
| — | Savings / ASV / PER commission (accounting + partners) |
- Funds boundary (UC-REG-04 /
UC-REG-09): a customer payment account is a sub-ledger position over
safeguarded Crédit Mutuel liquidity — not a Green-Got deposit, savings, or e-money. Billing may
move a fee internally from that position to Green-Got’s operating account, but payment settles a
receivable; it never creates revenue. This crate requests transfers;
core_bankingexecutes them. - Savings boundary (UC-BIL-15): savings/ASV/PER/livret bill the
customer nothing — no customer invoice. The only economic event is partner commission, which
lives in
accounting(UC-ACC-07), outside this crate. - Accounting boundary (UC-ACC-13): this crate emits source fields; it does not own GL mapping or journal generation.
- Billing during a freeze (UC-REG-05): a legal seizure / garnishment / regulatory freeze does not cancel the subscription — billing continues; if frozen funds block collection, the fee goes to arrears and follows dunning.
5. Related documents
- 2. Data Model — entities, ER diagram, invariants.
- 3. Billing Model — In Advance — bill-in-advance + collection rail.
- 9. Architecture — DDD layers and the accounting-source output.
../../docs/0_use_cases.md— the cross-crate use cases (UC-BIL-*).
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).
- Design rule — per-event granularity. Each fee is captured as one billable event and becomes one invoice line with its exact recognition date; a day’s fees are never collapsed into a single batched line (UC-BIL-04). Aggregation for the customer happens on the statement (UC-BIL-11), not on the invoice line.
- Design rule — recognition is the event date for one-off fees. A one-off fee’s
recognition_start = recognition_end = event date(UC-ACC-03); a subscription cycle’s recognition spans the service period it covers (UC-ACC-01). - Invariant: A captured billable event is billed exactly once. Its
statusmovescaptured → billedwhen its invoice line is issued; it is never billed twice.
3. Invoice and InvoiceLine
The immutable customer invoice. Every charge has one (UC-BIL-12).
- Invariant — immutability. An issued invoice is never mutated
(UC-BIL-12/24). Payments, partial payments, credit notes, and write-offs
change related rows (allocations, credit notes, status), never the invoice’s lines or totals. A
partial payment moves
statustopartially_paidand leaves the invoice body untouched (UC-BIL-24). See 4. Numbering & Immutability. - Invariant — gap-free number per Green-Got legal entity.
numberis assigned at issuance from Green-Got’s own gap-free sequence, distinct from theinvoicingcrate’s series (UC-BIL-12). - Design rule — gross / discount / net lines. A discounted offer bills a gross price line + a
discount line,
net = charged, preserving the gross-revenue-vs-discount distinction for accounting (UC-BIL-03, UC-ACC-05). A free-month cycle bills the gross line + a 100% free-month-credit line (net €0) (UC-BIL-17). - Design rule — line carries recognition + VAT. Each line carries its own
recognition_periodand its VAT (rate + amount) resolved from the line’s datedtax_code(UC-ACC-08, UC-ACC-12). See 7. VAT & Tax Codes. - Design rule — no €0 invoice. A €0 offer produces no invoice at all — not even a €0 one — but one-off fees on a free offer still bill normally (UC-BIL-13).
- Invariant —
amount_dueis derived.amount_due = Σ line gross-of-VAT net − Σ allocated payments − Σ applied credit. It is computed, never stored mutably on the invoice (UC-BIL-24). - Invariant — rounding snapshotted. Rounding is per line at issuance and snapshotted; it is never recomputed later (UC-BIL-23, UC-SUB-32).
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).
- Design rule — credit note vs write-off. A credit note is for when the customer no longer owes (error, dispute, gesture); an uncollectible valid debt is written off (revenue kept, loss recognised), not credit-noted (UC-BIL-10). See 5 §4.
- Design rule — mandatory reason code. Every credit note carries a
reason_codethat drives accounting classification (contra-revenue vs liability vs expense) (UC-BIL-16 / UC-ACC-06). See 6. Refunds & Reason Codes. - Design rule — settlement. A credit note either nets against the next invoice (default, no cash moves), becomes a refund liability when there is no future invoice to offset (e.g. a leaving customer, UC-BIL-09), or is otherwise applied. A credit note that becomes cash-back uses the Refund entity.
- Invariant — reverses revenue + VAT. A credit note reverses the corrected line’s revenue and the associated collected VAT (UC-ACC-09).
5. Payment and PaymentAllocation
Settlement of receivables by internal transfer (UC-BIL-05).
- Design rule — append-only ledger. Payments and their allocations are append-only; a correction is a new compensating row, never an edit.
- Design rule — oldest-first allocation, no minimum. A receipt is allocated to open balances oldest-first; partial payment is allowed with no minimum threshold — whatever is available is taken, however small (UC-BIL-24).
- Invariant — payment never creates revenue. A payment settles a receivable; it never books revenue (UC-BIL-05). Revenue arises from the invoice’s recognition, not from cash.
- Invariant — no over-allocation. Σ allocations against an invoice never exceeds its
amount_due; the account is never pushed negative for Green-Got’s own fee (UC-BIL-24, UC-SUB-14).
6. Refund
Cash returned to the customer for the non-waivable legal / correction cases (UC-BIL-09/14/21).
- Design rule — mandatory reason code + legal basis. Every refund carries a
reason_code(UC-BIL-16) and alegal_basis(withdrawal / statutory / error / termination). See 6. Refunds & Reason Codes. - Design rule — not a 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).
- Invariant — SLA on withdrawal refunds. A withdrawal/renunciation refund carries an
sla_deadlineand must be paid without undue delay (≤ 30 days) (UC-BIL-21).
7. Statement
A non-fiscal monthly aggregate of a customer’s billing activity (UC-BIL-11).
- Design rule — aggregates, never replaces, invoices. A statement shows opening balance, invoices issued, payments, credit notes, and closing balance for the period; one statement can reference several invoices (UC-BIL-11). It is a read-model rollup, not a fiscal document.
- Design rule — combined amount due across immutable invoices. When a shortfall is carried, the statement shows the combined amount due as one figure even though it spans two immutable invoices — the old shortfall is never rolled into the new invoice (UC-BIL-24).
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 — how invoices and billable events are created.
- 4. Invoice Numbering & Immutability — numbering and immutability invariants.
- 5. Partial Payment & Dunning — payment, allocation, write-off.
- 7. VAT & Tax Codes — line-level VAT resolution.
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).
- Design rule — first invoice on activation. A paid offer issues its first invoice when the subscription period opens (the obligation arises), independent of whether or when the charge clears (UC-BIL-01). The invoice exists even if collection later fails — a failed charge yields an unpaid invoice, never the absence of one (UC-BIL-06).
- Design rule — anniversary cadence. Each subsequent cycle bills in advance at the subscription’s anniversary date (or the earliest possible date before it); the anchor is per-subscription (UC-BIL-02).
- Design rule — a bundle bills as one charge. A bundle subscription bills as one charge for the offer, not one per module (UC-BIL-02).
- Invariant — recognition period on every recurring line. A subscription line carries
recognition_period = [period_start, period_end)soaccountingcan defer and release it prorata temporis (UC-ACC-01/12).
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:
- if the counter is > 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/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 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.
- Invariant — the anchor drifts. The billing anniversary moves to the switch date and drifts with each upgrade (UC-BIL-08).
- Design rule — downgrades produce no credit note. Perks are kept to period end; no mid-period proration credit (UC-BIL-08, UC-SUB-07).
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.
- Invariant — payment settles a receivable, never creates revenue. Collection settles an existing invoice receivable; revenue arises from the invoice’s recognition, not from the cash movement (UC-BIL-05).
- Invariant — funds boundary preserved. The customer payment account is a sub-ledger position over safeguarded funds — not a Green-Got deposit, savings, or e-money. The internal transfer must preserve the UC-REG-04 / UC-REG-09 boundaries; received payment-service funds are non-commingled and safeguarded by D+1.
- Design rule — partial collection, not nothing. If the balance cannot cover the full fee,
collection takes what is available and allocates it to the invoice, moving it to
partially_paid— the full failure path and dunning are in 5. Partial Payment & Dunning (UC-BIL-06/24). - Design rule — billing continues during a freeze. A legal seizure / garnishment / regulatory freeze does not cancel the subscription; if frozen funds block collection, the fee goes to arrears (UC-REG-05, UC-SUB-14).
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
- 2. Data Model — BillableEvent / Invoice / InvoiceLine structure.
- 4. Invoice Numbering & Immutability — numbering at issuance.
- 5. Partial Payment & Dunning — failed-charge handling.
- 7. VAT & Tax Codes — line-level VAT at issuance.
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:
- An issued invoice’s number is permanent; it cannot be changed and its slot cannot be reassigned.
- An issued invoice cannot be deleted to free a number — that would create a gap. Corrections use a credit note (UC-BIL-10/16), never deletion.
- Allocation is chronological: number n+1 is issued after number n within a series.
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:
- a partial payment moves
statusopen → partially_paidand records aPaymentAllocation; the invoice is not mutated (UC-BIL-24); - a correction issues a credit note referencing the invoice (UC-BIL-10/16);
- a write-off moves
statustowritten_offand recognises a loss; the invoice and its revenue are kept (UC-BIL-10 / UC-ACC-10).
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:
| Identifier | What it is | When it exists | Mutable? | Used for |
|---|---|---|---|---|
Internal id (inv_…) | Opaque, collision-proof, time-sortable key | At creation, before any number | Never | Primary key, FKs, event payloads, idempotency |
| Legal number | Gap-free {prefix}-{year}-{seq} per Green-Got legal entity | At issuance | Never, once assigned | The 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
- 1. Overview — the
invoicing-crate contrast. - 2. Data Model — the Invoice entity and status set.
- 5. Partial Payment & Dunning — what happens after issuance.
- 8. E-Invoicing & E-Reporting — transmission/reporting of issued invoices.
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 --> [*]
- Design rule — collection covers open balances oldest-first. Every collection attempt (and every
swept incoming fund) covers open balances oldest-first
(UC-BIL-24). A
PaymentAllocationrecords each application; the payment ledger is append-only (2. Data Model §5). - Design rule — the statement shows the combined amount due. The monthly statement (UC-BIL-11) presents the combined amount due (e.g. €15) as one figure, even though it spans two immutable invoices (UC-BIL-24).
- Invariant — never push the account negative for our own fee. Allocation never exceeds available funds and never drives the customer’s account negative for Green-Got’s own fee (UC-BIL-24, UC-SUB-14).
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:
| Offset | Action |
|---|---|
| −1 day | pre-charge notice / ensure-funds reminder before the attempt |
| +1 day | first retry after a failed/partial charge |
| +1 week | second retry |
| +1 month | third retry |
| +2 months | final retry; on continued failure the balance is escalated to write-off |
- Design rule — the cadence runs per open balance. Each immutable open invoice runs its own cadence; a carried shortfall and a new cycle’s invoice each dun independently (UC-BIL-24). The cadence is a Temporal workflow (9. Architecture); offsets are configuration, defaulting to the table above.
- Invariant — every retry is a fresh collection attempt under the same rules. A retry takes whatever is available (no minimum), allocates oldest-first, and never pushes the account negative (UC-BIL-24).
5. Write-off vs credit note
This is the decisive classification at the end of dunning, mapped from UC-BIL-10.
| Situation | Treatment | Accounting |
|---|---|---|
| Uncollectible valid debt (customer still owes, cannot pay) | Write-off — drop from active receivables | Revenue kept, loss recognised (créance irrécouvrable, UC-ACC-10) |
| Customer no longer owes (error / dispute / gesture) | Credit note, reason-coded | Reverses revenue (contra-revenue, UC-ACC-09) |
- Design rule — a write-off is not a credit note. A write-off keeps the original revenue and recognises a loss; it is not a revenue reversal and not a credit note (UC-BIL-10 / UC-ACC-10).
- Design rule — recovery reopens value. A written-off invoice that is later paid is recovered (UC-BIL-10); the sweep/collection machinery still applies to it.
- Invariant — neither path mutates or deletes the invoice. Both are status transitions / related documents over the immutable invoice (UC-BIL-12); a finalized invoice is never deleted to clear a debt (4 §4).
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
- 2. Data Model — Invoice status, Payment, PaymentAllocation.
- 3. Billing Model — In Advance — collection by internal transfer.
- 6. Refunds & Reason Codes — credit-note reason codes.
- 9. Architecture — the dunning workflow and the incoming-funds sweep rule.
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:
| Path | Trigger | Reference |
|---|---|---|
| Error correction (irreducible) | Fee charged in error or twice (bug, retry race) — returning money taken in error is legally required | UC-BIL-09, UC-BIL-16 |
| Payment-service statutory refunds | Unauthorised / 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 rights | A non-waivable rule requires pro-rata reimbursement of prepaid charges | UC-REG-01 |
| Cooling-off / rétractation | 14-day distance-financial-services withdrawal (and 30-day ASV renunciation) | UC-BIL-14/21 |
| Gesture that cannot be netted | A gesture credit note on an already-paid invoice with no future invoice to offset (leaving customer) becomes a refund liability | UC-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 class | Example | Modelled as | Accounting treatment |
|---|---|---|---|
| service not rendered | card not delivered | credit note reducing revenue; if already collected → refund liability until paid | contra-revenue (UC-ACC-09) |
| gesture reducing an invoice | app-down free month | credit note reducing revenue | contra-revenue (UC-ACC-05) |
| cashback / bonus / cash reward (not linked to an invoice) | spend reward | customer credit / pending-balance liability, not an invoice credit note | liability (UC-BIL-22) |
| pure compensation / indemnity (not reducing a price) | goodwill payout | expense, with customer liability until settled | expense |
| billing error / double charge | retry race | correction credit note + refund liability if already collected | contra-revenue + liability |
- Design rule — gesture defaults to contra-revenue. 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), by default a reduction of revenue (contra-revenue), not a marketing expense (UC-BIL-07). It is distinct from an offer-intrinsic promo (UC-OFF-03).
- Design rule — not invoice-linked → not a credit note. A gesture/reward not linked to a customer invoice (cashback, bonus, cash reward, pure compensation) is not modelled as an invoice credit note; it follows the reason-code treatment above (UC-BIL-07/16).
- Invariant — the reason code determines the GL bucket. The reason code is mandatory and maps each item to contra-revenue / customer-credit liability / expense; the GL treatment is never inferred from the entity type alone (UC-ACC-06).
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)
- Promo code (UC-BIL-19) — validated (validity window, per-customer single-use, eligibility); attaches a discount (gross + discount line, UC-BIL-03) or free months (UC-BIL-17) to the subscription’s modifiers, applied at the invoicing layer. Customer-specific, not offer-intrinsic.
- Referral / parrainage (UC-BIL-20) — on a qualifying event the referrer’s free-month counter is incremented (reason-coded); the referee may receive a symmetric credit. Referral rewards are regulated acquisition premiums (UC-REG-10).
- Cashback / spending rewards (UC-BIL-22) — credited as a customer-credit / pending-balance liability, not an invoice credit note, reason-coded. Funding is explicit (business-card interchange ~2%, or partner-funded), not generic “interchange” (consumer debit interchange is capped at 0.2% under the IFR and cannot fund meaningful cashback). Distinct from an acquisition prime (UC-REG-10) and a commercial gesture (UC-BIL-07).
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).
- Payment-account / card subscription — 14-day distance-financial-services withdrawal (C. consommation L222-7 et seq.); within the window, reverse the activation fee (full or pro-rata per regime) via the refund path (UC-BIL-14).
- ASV / life products — 30-day renunciation (C. assurances L132-5-1); PER per its own regime. Renunciation triggers partner commission clawback (UC-ACC-15), not a customer-invoice refund.
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):
- a withdrawal function available throughout the withdrawal period;
- a durable-medium acknowledgement of receipt of the withdrawal;
- refund within the statutory SLA (without undue delay, ≤ 30 days);
- proof of precontractual-information delivery on a durable medium — if not delivered, the withdrawal period is extended.
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
- 2. Data Model — CreditNote and Refund entities.
- 5. Partial Payment & Dunning — write-off vs credit note.
- 7. VAT & Tax Codes — VAT recovery on a credit note.
- uncertainties.md — open reason-code, withdrawal, and reward items.
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 type | Tax treatment | Basis |
|---|---|---|
| Core banking / payment services | within the art. 261 C exemption perimeter | UC-ACC-08 |
| Green-Got’s subscription | provisionally taxable at 20% under the art. 260 B option | UC-ACC-08 |
| Non-banking service fees | taxable by default unless their own tax code says otherwise | UC-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-Got | UC-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
- 2. Data Model — InvoiceLine VAT fields.
- 3. Billing Model — In Advance — VAT resolved at issuance.
- 6. Refunds & Reason Codes — VAT on credit notes.
- 8. E-Invoicing & E-Reporting — e-reporting scope by VAT status.
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):
| Limb | What it transmits | Applies to | Recipient |
|---|---|---|---|
| E-invoicing (invoice transmission) | the structured invoice | domestic B2B only — invoices to French VAT-taxable business customers, via a PDP | the buyer (via PDP) |
| E-reporting (transaction-data transmission) | aggregated daily transaction data (amount invoiced + VAT collected) | VAT-taxable B2C revenue, via a PDP | the tax authority — not the customer |
- Design rule — B2C consumer invoices are never transmitted to the individual. A B2C consumer invoice is never transmitted to the individual customer over the e-invoicing channel (UC-BIL-18); the consumer simply receives their invoice/statement. Only aggregated B2C data goes to the tax authority via e-reporting.
- Design rule — e-reporting scope follows VAT status. VAT-exempt payment-service fees
(art. 261 C, UC-ACC-08) are out of e-reporting scope; the
VAT-taxable subscription (260 B option) is in scope
(UC-BIL-18). The line’s snapshotted
tax_codedecides inclusion — see 7. VAT & Tax Codes §5.
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
- 1. Overview — Green-Got-as-issuer vs the
invoicingcrate. - 4. Invoice Numbering & Immutability — the issued artefact.
- 7. VAT & Tax Codes — VAT status that scopes e-reporting.
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/:
| Layer | Holds | Notes |
|---|---|---|
domain/ | Money, BillableEvent, Invoice/InvoiceLine, CreditNote, Payment/Allocation, Refund, Statement, ReasonCode, TaxCode, numbering | entities + 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_statement | orchestration 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 client | technical 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 workflows | the 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
billing_cycle_workflow— fires at the subscription anniversary, issues the in-advance invoice (interrogating the free-month counter), and triggers collection (UC-BIL-01/02/17).dunning_workflow— runs the −1d/+1d/+1wk/+1mo/+2mo retry cadence per open balance, escalating to write-off on exhaustion (UC-BIL-06 / UC-SUB-14). See 5. Partial Payment & Dunning.on_charge_resultrule — maps an internal-transfer result topaid/partially_paid/unpaidand allocates oldest-first (UC-BIL-24).on_incoming_fundsrule — sweeps incoming funds to the oldest outstanding balance before they can be spent (UC-BIL-24).refund_sla_workflow— drives a withdrawal refund within the statutory SLA (UC-BIL-21).
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):
- gross revenue, discounts / credit notes;
- VAT / tax code and rate (UC-ACC-08);
- recognition period (line-level, drives deferral/accrual and period cut-off, UC-ACC-01/12);
- receivable / payment state (open-balance source for the Σ-receivables = GL reconciliation, UC-ACC-11);
- refund / customer-credit liability;
- reason code (UC-BIL-16 / UC-ACC-06);
- product / module reference and customer / legal-entity references.
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
../plan.md— the concrete module layout.- 2. Data Model — the entities the layers operate on.
- 5. Partial Payment & Dunning — the dunning workflow and sweep rule.
- 7. VAT & Tax Codes — the VAT source fields.
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.
- Class:
legal· Owner: Legal + Product → Backend · Due: Before paid-signup go-live · Launch-blocking: yes - Evidence: UC-BIL-14, UC-BIL-21; 6. Refunds & Reason Codes §4
- Status: OPEN (owned with legal)
- Port to: 6. Refunds & Reason Codes §4
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.
- Class:
product-decision/accounting· Owner: Product + Accounting → Backend · Due: Before accounting-source export go-live · Launch-blocking: yes - Evidence: UC-BIL-16, UC-ACC-06; 6. Refunds & Reason Codes §2
- Status: OPEN
- Port to: 6. Refunds & Reason Codes §2
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).
- Class:
product-decision· Owner: Product + Risk → Backend · Due: Before promo/referral launch · Launch-blocking: no (gates promo/referral features, not core billing) - Evidence: UC-BIL-19, UC-BIL-20; 6. Refunds & Reason Codes §3
- Status: OPEN
- Port to: 6. Refunds & Reason Codes §3
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.
- Class:
product-decision/tax· Owner: Product + Tax → Backend · Due: Before cashback launch · Launch-blocking: no (gates cashback feature) - Evidence: UC-BIL-22, UC-ACC-06; 6. Refunds & Reason Codes §3
- Status: OPEN
- Port to: 6. Refunds & Reason Codes §3
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).
- Class:
tax/legal· Owner: Tax advisor + Finance → Backend · Due: Before subscription-VAT go-live · Launch-blocking: yes - Evidence: UC-ACC-08, UC-BIL-23; 7. VAT & Tax Codes §3
- Status: OPEN (the dated
tax_codepolicy mechanism is designed; the value is provisional) - Port to: 7. VAT & Tax Codes §3
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
- 6. Refunds & Reason Codes — withdrawal, reason codes, promo/referral/ cashback.
- 7. VAT & Tax Codes — the 260 B / 261 C baseline.
../../docs/0_use_cases.md— upstream use cases and their ⚠️ OPEN flags.
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
- Offer — a priced commercial SKU. Composes Modules and declares the required Steps an onboarding must satisfy. Its contractual basis (price, composition, entitlements) is immutable/versioned; only non-material presentation fields are editable in place. [UC-OFF-06], [UC-OFF-14], [UC-ONB-02]
- Module — an atomic functional unit from the shared Module catalogue (payment/current account, shared account, card sub-module, livret, ASV, PER). Each module spec carries category, per-person cap, dependencies, tier, tax code, fallback, regulated nature. [UC-OFF-14]
- Step — a reusable onboarding requirement from the shared Step dictionary
(
get_email,get_phone,personal_info,siret_lookup,id_doc_pvid,recap,submit_for_review, …). An offer declares the subset it requires. The dictionary lives only in code (onboarding’sSTEP_REGISTRY): each Step’scode,nameandkindare typed trait methods — there is nostep_dictionarydatabase table. An offer references Steps bycode(offer_required_step); a boot assertion fails if a referencedcodeis not in the registry. [UC-ONB-02] - Tier — Essential / Premium; lives on the Module, never on the account. [UC-SUB-16]
- Visibility —
PubliclyAccessible(default) vsRestricted(surfaced only to whitelisted identities). [UC-OFF-12] - Segment — Retail vs Business; determined by the calling API. [UC-ONB-01]
- Campaign code — a back-office-defined code (deeplink or typed) that surfaces specific offers / launches a specific onboarding; QR event-cards and whitelists are instances of this one primitive. [UC-ONB-01], [UC-ONB-24]
Onboarding
- Onboarding (session) — one in-flight attempt to subscribe to one offer, owned by a single identity; at most one in-flight per owner. [UC-ONB-02], [UC-ONB-03], [UC-ONB-28]
- Onboarding owner — the identity a session belongs to: an anonymous prospect token (logged-out “open an account”, empty DataBag) or an authenticated user (in-app, pre-filled). Authorization matches the caller to the owner; mismatch = not-found. [UC-ONB-28]
- Prospect session — the httpOnly prospect token minted when a logged-out prospect picks an offer;
the concrete form of the anonymous owner and the sole handle to the onboarding, with an empty DataBag.
It is short-lived: the
get_emailStep verifies the email, creates the user and an authenticated session, and promotes the owner to thatuser_id([UC-ONB-31]). It grants no access to real data (no account exists yet) throughout. [UC-ONB-28], [UC-ONB-31] get_email/get_phone— the two verified contact Steps that replace the oldcontact_detailsandmobile_otpsteps.get_email(first) verifies the email by OTP, then creates the user + verified email login-identifier and mints an authenticated session (passwordless,EmailOtpcredential).get_phone(second) verifies the phone by OTP and adds a verified phone login-identifier; its provider is channel-selected (web → Vonage, mobile → Infobip silent-auth, deferred). [UC-ONB-31], [UC-ONB-33]- Channel —
web|mobile, carried on the contact-Step submissions; selects theget_phoneprovider and the minted session’s device type / lifetime. [UC-ONB-33] - Duplicate-identity rule — checked per contact Step: the email at
get_email, the phone atget_phone. If a submitted credential already belongs to a login-identifier, no user/identifier/session is created: the prospect is told the credential already exists and routed to log in. [UC-ONB-32] - Promotion / claim — an anonymous prospect is promoted to a real user at
get_email(auth identity) and the customer is provisioned at validation ([UC-ONB-29]); a prospect who signs in mid-flow claims their session if they have none in flight ([UC-ONB-30]). - DataBag — the transient per-onboarding working state (collected data points). Hydrated from the canonical KYC/customer master at pre-fill; flushed back on validation. Not the source of truth. [UC-ONB-26]
- Resolver — computes the next missing Step as
required − satisfied, topologically ordered by each Step’srequires. [UC-ONB-03] - Required-step snapshot — the offer’s required-Step list, frozen into the session at creation (an offer/Step-set change mid-flight archives the session). [UC-ONB-23]
- Officer validation — a submitted onboarding is validated automatically (low-risk) or by a compliance officer (regulated decision). [UC-ONB-05], [UC-ONB-25]
- Provisioned customer — the onboarding-domain record written at validation (this v1 slice writes no real person/organisation/bank-account aggregates). [UC-ONB-05]
- Officer comment / request changes — an officer can send a submitted onboarding back, marking specific Step(s) unmet with a comment; the resolver re-surfaces them. [UC-ONB-06]
Subscriptions
- Subscription — the living holder↔offer relation. Snapshots the offer/module versions + price; carries dates, cadence, status, and customer-specific modifiers; links its predecessor by filiation. [UC-SUB-32]
- Account — the banking object (IBAN, balance, ring-fence); tier-agnostic. Each active account is bound to exactly one active subscription. [UC-SUB-12]
- Holder / Participant — the holder owns the account and the money; a participant is an authorized user (own card), zero ownership. A minor is a constrained participant on a parent’s account. [UC-ONB-11], [UC-ONB-12], [UC-SUB-26]
- Offer change — always close-then-open. Upgrade = immediate when collectable; downgrade = keeps perks to the period boundary. [UC-SUB-06], [UC-SUB-07]
- Bundle — one subscription covering several modules at one price. [UC-SUB-04]
- Period / anniversary / boundary — the span a charge covers / the renewal anchor / the instant one period ends. [UC-SUB-31]
- Modifier — a customer-specific adjustment (
free/employeetag, free-month counter, promo discount) applied at the invoicing layer, never on the offer. [UC-SUB-29] - Suspension — an AML/fraud-only subscription state (never for unpaid fees). [UC-SUB-22]
Billing & accounting
- BillableEvent — a chargeable event captured at the moment it happens (ATM, FX, reissue), one invoice line at its recognition date. [UC-BIL-04]
- Invoice — immutable, gap-free numbered per Green-Got legal entity; subscriptions billed in advance. [UC-BIL-01], [UC-BIL-12]
- Credit note — reduces/reverses an invoice (gesture, error, service-not-rendered), reason-coded. [UC-BIL-07], [UC-BIL-16]
- Payment / partial payment — collection by internal transfer (no SEPA DD); a shortfall is carried forward oldest-first. [UC-BIL-05], [UC-BIL-24]
- Refund vs payout — refunds are commercial-no / legal-yes (error, statutory, cooling-off); returning a customer’s own balance on closure is a payout, not a refund. [UC-BIL-09]
- Statement — a non-fiscal periodic aggregate of a customer’s invoices, payments, credit notes. [UC-BIL-11]
- Recognition / deferred revenue — fee billed in advance is earned over the period; the unearned part sits in deferred revenue (PCA). [UC-ACC-01]
- Partner commission / clawback — savings revenue paid by the partner (no customer invoice), exposed to clawback on early exit; recognised net of a provision. [UC-ACC-07], [UC-ACC-15]
- Reason code — mandatory classifier on every credit/refund/reward that drives the GL treatment (contra-revenue / liability / expense). [UC-BIL-16], [UC-ACC-06]
- Tax code — dated VAT classification resolved at issuance (261 C exemption vs the 260 B taxable option). [UC-ACC-08]
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
- 1. Offers Overview — scope, the Offer/Module/Step vocabulary, what the crate owns vs references, and the split with subscriptions and onboarding.
- 2. Data Model — the entities (Offer, ModuleCatalogueEntry, StepDictionaryEntry, OfferModule, OfferRequiredStep, whitelist), the ER diagram, and the per-entity invariants, each traced to its UC.
Lifecycle, composition & access
- 3. Offer Lifecycle and Versioning — Draft/Active/Retired,
start/end dates and retirement ([UC-OFF-05]), and immutable
(code, version)material-vs-editable versioning ([UC-OFF-06]). - 4. Module and Step Catalogue — the shared Module catalogue ([UC-OFF-14]) and Step dictionary ([UC-ONB-02]), per-offer composition, and the collision/derivation rules ([UC-ONB-18]).
- 5. Eligibility, Visibility and Fallback — continuous eligibility ([UC-OFF-04]), visibility & the restricted-offer whitelist ([UC-OFF-12]), declared-fallback integrity ([UC-OFF-11]/[UC-OFF-13]) and the per-module standard base offer ([UC-OFF-07]).
Architecture
- 6. Architecture — DDD layers (domain / use_cases / stores), the store map,
the read API surface (
list_available_offers,admin_*), and the seed.
Other
- Uncertainties — the active, classified register of open offers items (260 B subscription tax code, eligibility validity windows, restricted-offer whitelist edge cases), each with class, owner, due, launch-blocking flag and evidence link.
To Be Created
- Campaign-code resolution ([UC-ONB-24]) — the loose code→config mechanism that surfaces specific offers; document the offers-side hook when scoped.
- Upgrade/downgrade relationship metadata ([UC-OFF-09]) — document when the switch-classification rules are built out alongside subscriptions.
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):
- Offer — a priced commercial SKU. It composes Modules ([UC-OFF-14]) and declares the required Steps an onboarding must satisfy ([UC-ONB-02]). It is immutable/versioned on its contractual basis ([UC-OFF-06]).
- Module — an atomic functional unit from the shared Module catalogue (the Bibliothèque de
modules):
payment_account,shared_account,card(a sub-module),livret,assurance_vie,per. Defined once, reused across offers. - Step — a reusable onboarding requirement from the shared Step dictionary:
personal_info,id_doc/PVID,mobile_otp,proof_of_address,funds_origin,topup,siret_lookup,kyb_docs,recap, … Defined once, reused across offers. This crate stores the definition; the executable behaviour lives in the onboarding crate, keyed by the samecode.
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.
| Owns | References (owned elsewhere) |
|---|---|
| Offer SKU; Module catalogue; Step dictionary; the two composition tables | the banking account / IBAN / ledger (core_banking) |
| Offer eligibility hook; restricted-offer whitelist; visibility | identity / KYC + the executable Step behaviour (physical_person + onboarding) |
Lifecycle (Draft/Active/Retired), start/end dates, versioning; is_primitive/fallback marking | the 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
- Customer-facing subscribable-offers gate —
list_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. - Back-office catalogue views —
admin_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)
- Invariant — single dictionaries. There is exactly one Module catalogue and one Step dictionary; an offer references entries by id, never inlines a module or step ([UC-OFF-14], [UC-ONB-02]).
- Invariant — the contractual basis is immutable. Price, module composition and contractual
entitlements never mutate in place; a material change is a new
(code, version)row ([UC-OFF-06]). - Invariant — no segment imports. This crate compiles without any segment/app dependency (Appendix A).
5. Related Documents
- 2. Data Model — entities, ER diagram, per-entity invariants.
- 3. Offer Lifecycle and Versioning — lifecycle + versioning.
- 4. Module and Step Catalogue — catalogue, dictionary, composition, collision.
- 5. Eligibility, Visibility and Fallback — eligibility, whitelist, fallback integrity.
../../docs/0_use_cases.md— the cross-domain use-case spine.
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.rswith nooffer/module_catalogue/offer_module/offer_required_step/offer_whitelist_entrytables. 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]).
| Field | Type | Notes | Trace |
|---|---|---|---|
id | OfferId (off_…) | Prefixed, time-sortable. | |
code | string | Stable product code (e.g. sole_trader). Material/immutable. | [UC-OFF-06] |
version | int | Append-only version of code. Material/immutable. | [UC-OFF-06] |
name | string | Display name. Presentation/editable in place. | [UC-OFF-06] |
available_countries_of_residence | CountryCode[] | 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_forms | LegalForm[] | 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] |
segment | Segment = Retail | Business | Plain field; the gate is applied by the reader, not by importing segment code. | [UC-ONB-01] |
price_cents | i64 | Recurring price in minor units; 0 is a valid free offer ([UC-OFF-08]). Material. | [UC-OFF-06] |
currency | ISO 4217 | EUR for now. | |
visibility | Visibility = PubliclyAccessible | Restricted | Default publicly accessible; restricted = whitelist-only. | [UC-OFF-12] |
lifecycle | Lifecycle = Draft | Active | Retired | Subscribable only when Active and in-window. | [UC-OFF-05] |
start_at | timestamp | Start of the subscribable window. | [UC-OFF-05] |
end_at | timestamp, nullable | Optional end; after it the offer is retired. | [UC-OFF-05] |
is_primitive | bool | A self-standing terminal fallback target (fallback = self); needs no declared fallback. | [UC-OFF-13] |
group_code | string | Variant 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
subscriptionsforce-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.
| Field | Type | Notes | Trace |
|---|---|---|---|
id | ModuleId (mod_…) | Prefixed, time-sortable. | |
code | string, unique | e.g. payment_account, shared_account, card, livret, assurance_vie, per. | [UC-OFF-14] |
category | ModuleCategory | Exclusivity class (e.g. current_account), used for collision. | [UC-OFF-14] |
per_person_cap | int | Per-person uniqueness (e.g. one current_account). | [UC-SUB-11] |
dependencies | ModuleId[] | Required other modules (livret/ASV/PER require a current_account). | [UC-SUB-11] |
tier | string, nullable | Essential | Premium where applicable; drives entitlements. The account has no tier (Appendix A). | [UC-OFF-14] |
tax_code | string | VAT/tax classification (effective-dated policy lives in accounting). | [UC-ACC-08] |
regulated_nature | RegulatedNature | banking | 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.
| Field | Type | Notes | Trace |
|---|---|---|---|
id | StepId (stp_…) | Prefixed, time-sortable. | |
code | string, unique | e.g. personal_info, id_doc, mobile_otp, proof_of_address, siret_lookup, kyb_docs, recap. | [UC-ONB-02] |
name | string | Human label. | |
kind | StepKind | Coarse 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]).
| Field | Type | Notes |
|---|---|---|
offer_id | OfferId | The composing offer. |
module_id | ModuleId | A 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]).
| Field | Type | Notes |
|---|---|---|
offer_id | OfferId | The offer. |
step_id | StepId | A dictionary entry it requires. |
ordinal | int | Order 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]).
| Field | Type | Notes |
|---|---|---|
offer_id | OfferId | The restricted offer this entry grants access to. |
email | string, nullable | A claimed (not necessarily pre-verified) email. |
phone | string, nullable | A 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.
OfferWhitelistEntryand the whitelist match are not built. Until they land, aRestrictedoffer is unreachable for everyone, so the in-code catalogue is guarded against shipping one: theno_restricted_offers_until_whitelist_existscatalogue test fails at CI/boot on anyVisibility::Restrictedoffer.
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:
Draftis catalogue-authoring state: the offer is not surfaced bylist_available_offersand may be deleted. Composition and required steps are set here.Activeis the only sellable state, and only within[start_at, end_at]. The subscribable gate ([UC-ONB-01]) returns an offer only ifActiveand the current time is in-window.Retiredis reached atend_ator by an explicit retire. It is terminal for new sales; it is not a closure of live subscriptions.
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:
- Contractual basis — immutable, versioned (append-only). Price (
price_cents), module composition (theOfferModuleset), and key contractual terms/entitlements. A change is a new(code, version)row, never a mutation. Moving the existing base onto the new version is an offer change / forced migration ([UC-SUB-06]/[UC-SUB-17]), routed through framework-change notice where it raises price or reduces rights ([UC-REG-01]). There is deliberately no “edit the price of a live offer”: raising the price is always a new offer (e.g. Essential 2028). - Non-material fields — editable in place. Presentation and non-contractual detail —
selection-screen copy, marketing description, partner-benefit wording, perk presentation,
name. The edit applies to everyone on the offer (including grandfathered subscriptions), which is safe because nothing the customer gets or pays changes. These fields are not part of the subscription snapshot ([UC-SUB-32]); they are read live.
Design rule — the classification test. Does the change alter what the customer gets or pays (price, composition, a contractual entitlement)?
- Yes → material → new
(code, version)row. - No → presentation → in-place edit.
⚠️ 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
- 2. Data Model — the Offer fields and the material/presentation flags.
- 4. Module and Step Catalogue — composition (part of the material basis) and required steps.
- 5. Eligibility, Visibility and Fallback — retirement + fallback integrity.
../../docs/0_use_cases.md— the use-case spine.
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):
category— exclusivity class (e.g.current_account), used for collision.per_person_cap/ uniqueness — e.g. onecurrent_account, one of each savings product per person ([UC-SUB-11]).dependencies— required other modules (livret/ASV/PER require acurrent_account, [UC-SUB-11]).tier— Essential/Premium where applicable, driving entitlements (the account is tier-agnostic; tier lives on the module, Appendix A).tax_code— VAT/tax classification ([UC-ACC-08]).- fallback — standard unit offer to fall back to, or
selffor a primitive ([UC-OFF-07]/[UC-OFF-13]); see 5. Eligibility, Visibility and Fallback. regulated_nature— banking / intermediated-savings (IOBSP) / insurance (IAS), routing conduct rules ([UC-REG-06]).
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:
- Unit offer — 1 module (e.g. Essential payment account), the canonical sellable ([UC-OFF-01]).
- Bundle offer — n modules at one price (e.g. current + shared); one offer → one subscription with multiple modules ([UC-OFF-02]/[UC-SUB-04]).
- Free offer —
price_cents = 0, still fee-capable for one-off events ([UC-OFF-08]).
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
- 2. Data Model — the catalogue/dictionary/composition entities.
- 5. Eligibility, Visibility and Fallback — the per-module standard base offer (fallback) and visibility.
../../docs/0_use_cases.md— the use-case spine.
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.
offersowns the declarations here: the eligibility rule, the visibility flag, the whitelist, the fallback target andis_primitive. The continuous re-evaluation at each period boundary and the execution of the fallback offer change live in thesubscriptionscrate ([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:
PubliclyAccessible(default) — surfaced to anyone passing eligibility, collision, country and segment checks ([UC-ONB-01]/[UC-ONB-18]).Restricted— never shown by default; surfaced only to whitelisted identities. AnOfferWhitelistEntrymaps an email/phone to the restricted offer it may access. Using a whitelisted email/phone at the subscribable-offers gate makes those restricted offers visible and subscribable; everyone else has no access.
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
OfferWhitelistEntrywhitelist and the whitelist-match at the subscribable-offers gate above are not built yet. With no whitelist to surface them, aRestrictedoffer 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: theno_restricted_offers_until_whitelist_existscatalogue test fails if any offer declaresVisibility::Restricteduntil 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
subscriptionsforce-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
- 2. Data Model — the visibility flag, whitelist entry, and
is_primitive. - 3. Offer Lifecycle and Versioning — retirement and grandfathering.
- 4. Module and Step Catalogue — the per-module fallback derivation.
../../docs/0_use_cases.md— the use-case spine.
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 onboardingSTEP_REGISTRY). There is nooffer/module_catalogue/offer_module/offer_required_step/offer_whitelist_entrytable 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/)
catalogue.rs— the staticOFFERS/MODULESregistry plusfind_offer/find_modulelookups and the catalogue invariant tests (sibling step-set equality, id/(code,version)/module-code uniqueness, and theno_restricted_offers_until_whitelist_existsguard).domain/—OfferId/ModuleId,Offer,ModuleCatalogueEntry,StepDictionaryEntry,OfferModule,OfferRequiredStep, and theSegment/Visibility/Lifecycle/ModuleCategory/RegulatedNature/StepKindenums. No I/O.use_cases/— orchestrates domain objects + catalogue reads; no business logic leaks into routes.stores/— pure, in-memory lookups overcatalogue.rs(thestores/name is kept per the architecture skill’s folder shortcut, but there is no Postgres access here). Steps have no DB id: the dictionary lives in the onboarding registry, keyed bycode.
2. Catalogue-access map
The stores/ functions are pure reads over the in-code catalogue (no tables):
| Module | Backed by | Responsibility |
|---|---|---|
offer_store | catalogue::OFFERS | Find/list offers; sellability + start gate; (country, segment, legal_form) availability; variant-group siblings; back-office listing. |
catalogue_store | catalogue::MODULES / OFFERS | The 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]
- country + segment gate — offers always differ by country; segment is a plain field, the gate is applied here, not by importing segment code ([UC-ONB-01]).
- visibility/whitelist —
Restrictedoffers only for whitelisted identities ([UC-OFF-12]). - collision — exclude offers whose module category the customer already holds one-per-person,
derived from module
category+per_person_cap([UC-ONB-18]); the upgrade is surfaced bysubscriptions, not here.
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:
- the onboarding orchestrator — to build a session from an offer’s
OfferRequiredStepset ([UC-ONB-02]); subscriptions— to snapshot a frozen offer/module version + price ([UC-SUB-32]) and to drive continuous eligibility / fallback transitions ([UC-OFF-04]/[UC-OFF-11]).
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:
sibling_offers_share_an_identical_step_set— siblings declare the same ordered step set.catalogue_identifiers_are_unique— offer id,(code, version), module code, and per-offer step codes are unique (the in-code stand-in for DB unique constraints).no_restricted_offers_until_whitelist_exists— guards against shipping an unreachableRestrictedoffer while the whitelist is unbuilt (docs 2 §7 / 5 §3).assert_registry_covers_offers(onboarding, run atbusiness_apiboot) — every offer’s required step codes resolve in the onboardingSTEP_REGISTRY.
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:
| Store | Table | Responsibility |
|---|---|---|
offer_store | offer | CRUD on offer rows; query Active + in-window by (country, segment). |
module_catalogue_store | module_catalogue_entry | The shared module library. |
step_dictionary_store | step_dictionary_entry | The shared step dictionary (definitions only). |
offer_module_store | offer_module | Offer→module composition. |
offer_required_step_store | offer_required_step | Offer→ordered-required-step declaration. |
whitelist_store | offer_whitelist_entry | Restricted-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
../plan.md— the build-oriented module layout.- 2. Data Model — entities and invariants.
- 4. Module and Step Catalogue — the dictionaries and collision.
- 5. Eligibility, Visibility and Fallback — the gate’s visibility and fallback rules.
../../docs/0_use_cases.md— the use-case spine.
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 toaccounting. 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.
- Class:
legal· Owner: Claude (research) → Accounting / external tax advisor · Due: Before first billed subscription · Launch-blocking: no (offers stores the code; computation + the launch-blocking gate sit inaccounting/customer_billing) - Evidence: 2. Data Model §3, 4. Module and Step Catalogue §1; spine Appendix B (ACC-08), Appendix A VAT premise
- Status: RESEARCH PENDING (Claude)
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.
- Class:
product-decision· Owner: Claude (research) → Product / Backend (subscriptionsowns execution) · Due: Before conditional/bundle offers ship · Launch-blocking: no (the sole-trader launch offer carries no continuous condition) - Evidence: 5. Eligibility, Visibility and Fallback §1; spine Appendix B (ONB-10), [UC-OFF-04]
- Status: RESEARCH PENDING (Claude)
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.
- Class:
product-decision· Owner: Claude (research) → Product / Backend · Due: Before the first restricted (closed-beta) offer · Launch-blocking: no (the sole-trader launch offer isPubliclyAccessible) - Evidence: 5. Eligibility, Visibility and Fallback §3, 2. Data Model §7; spine [UC-OFF-12], [UC-ONB-24]
- Status: RESEARCH PENDING (Claude)
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.
- ✅ Ported: 5. Eligibility, Visibility and Fallback §4, 2. Data Model §2.
- Class:
product-decision· Owner: Product / Backend · Launch-blocking: no - Source: spine [UC-OFF-13]/[UC-OFF-07]
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.
- ✅ Ported: 5. Eligibility, Visibility and Fallback §3.
- Class:
product-decision· Owner: Product · Launch-blocking: no - Source: spine [UC-OFF-12], Appendix A visibility premise
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.
- ✅ Ported: 4. Module and Step Catalogue §5, 2. Data Model §3.
- Class:
product-decision· Owner: Backend · Launch-blocking: no - Source: spine [UC-ONB-18]/[UC-OFF-14]
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
- 2. Data Model —
tax_code, whitelist entry,is_primitive. - 4. Module and Step Catalogue — module specs and derived collision.
- 5. Eligibility, Visibility and Fallback — eligibility windows, whitelist, fallback integrity.
../../docs/0_use_cases.md— the use-case spine (Appendix B rollup).
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
- 1. Onboarding Overview — the orchestrator role, what moved and was generalized, and the domain boundaries.
- 2. Engine: Resolver, Registry, DataBag — the
Steptrait, the staticStepRegistry,DataBag/DataPoint, and the resolver (required − satisfied, topo byrequires). - 3. Step Dictionary & Sole-Trader Steps — the shared code-only dictionary ([UC-ONB-02]) and the concrete v1 sole-trader Steps (
get_email,get_phone, …) with theirprovides/requires/adapter.
Lifecycle & provisioning
- 4. Onboarding Lifecycle & Status — the session status state machine; submit, validate, request-changes, resume, abandon, expire.
- 5. Validation & Provisioning — officer-always validation ([UC-ONB-25]), the validation freeze that makes the session itself the provisioning record ([UC-ONB-05]), and the idempotent provisioning saga ([UC-ONB-27]).
Integrations & data
- 6. Integrations & Adapters — INSEE SIRET lookup, OTP send, and the identity-verification (PVID) provider, with environment credentials.
- 7. Data Model —
OnboardingSession(incl. the validation-freeze fields),OfficerComment, theonboarding_eventlog, and the ER diagram. - 8. Architecture — the DDD layers, stores, use cases (customer + BO), and the API surface across
business_apiandback_office_api.
Client integration
- 10. Retail Mobile Integration (v1) — implementation guide for the mobile app: the v1 POC call sequence, credential/header lifecycle (prospect token, device binding), the two-phase OTP steps, the
SubmitStepResultmodel, resume, and the phase-2 (mint/signing) preview.
Risk & compliance signals
- 9. Onboarding Events & Risk Signals — the append-only event log (server + obfuscated client meta, [UC-ONB-35]), the
pep_declarationStep ([UC-ONB-36]), and the back-office risk panel with derived global device/IP/country signals ([UC-BO-12]). Sanctions/watchlist screening stays out of scope ([UC-REG-02]).
Open items
- Open Items — the clean register of what is still open: provisioning-saga compensation ([ONB-U4]) and the duplicate-identity enumeration message ([ONB-U8]) — plus the one decided-but-not-yet-wired launch task (PVID onboarding seam + live Ubble credentials). Resolved decisions live in their canonical docs, not here.
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:
- 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.
- 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]):
- Anonymous prospect — started from the logged-out “open an account”
surface. Picking an offer mints an opaque, server-side prospect token (httpOnly
cookie) — the sole handle to the session, with an empty DataBag. The flow then
begins with the two contact steps in the offer’s snapshot order (email-first
on the B2B web flow, phone-first on retail): each verifies its identifier by OTP,
and the verification that completes the set (both
EmailVerifiedandPhoneVerified) on a fresh identity creates the user with both verified identifiers, mints an authenticated session, and promotes the owner prospect→user_id([UC-ONB-31]); a duplicate identity routes to login instead ([UC-ONB-32]). Only that thin auth shell exists yet — no person or account — and the DataBag holds just the verification facts ([UC-ONB-26]). - Authenticated user — started inside the app by a logged-in user (e.g. an
existing customer subscribing to a second offer, [UC-J-03]). Owned by the
session’s
user_id; the DataBag is pre-filled from canonical KYC ([UC-ONB-10]).
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.
| Concern | Old (B2B-only) | New (generalized) |
|---|---|---|
| Engine unit | Module | Step — same trait shape (id/provides/requires/applies_to/on_complete) |
| Required-data source | hardcoded Product::required_data_points() | offer-driven: the offer’s offer_required_step rows ([UC-ONB-02]) |
| Per-session requirement set | derived live from the Product enum | snapshotted into the session at create; immutable for the session’s life ([UC-ONB-03]/[UC-ONB-23]) |
| Step library | B2B Steps only | one shared dictionary for all segments, gated by applies_to(offer, bag) ([UC-ONB-13]) |
| Changing path | product_locked + partial reset | a different offer is a fresh onboarding; offer/Step-set change mid-flight ⇒ archive ([UC-ONB-23]) |
| End state | hand-off to organisation KYB workflow | validation 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).
| Owns | References (owned elsewhere) |
|---|---|
| The onboarding session + its status lifecycle | The 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 resolver | AML / compliance decisions (compliance) |
| Validation routing + the validation freeze on the session | The 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:
- In scope: the
Step/registry/resolver engine; the six sole-trader Steps (3); the status lifecycle (4); validation freezing the session (5); the INSEE/OTP/PVID adapters (6). - Out of scope (modelled, not built): real
physical_person/organisation/core_bankingprovisioning ([UC-ONB-05]); top-up/funding Steps ([UC-ONB-20]/[UC-ONB-21]); BO-declaration / KYB-docs / retail savings Steps; the subscribable-offers gate and eligibility ([UC-ONB-01]/[UC-ONB-18]); migration-as-onboarding ([UC-MIG-03], wired once real provisioning lands).
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
- The onboarding is create-first: you create it before collecting any data,
then submit one step at a time. The backend’s resolver always tells you the
current step; you never hardcode the order. The retail start needs no
body —
offerIddefaults to the free offer andcountrytoFR; the user picks their real plan later at theconfirm_offerscreen, and switching siblings is lossless (they share the step set, nothing is re-asked). - The whole v1 flow is owned by a prospect token (an opaque secret returned when you start). You send it on every call until — in phase 2 — the mint replaces it with a bearer session token.
- You drive it off
currentStep. After each submit, the response tells you the next step (or a field error). Render whatevercurrentStep.codesays; don’t assume a fixed sequence beyond what this guide shows. - Wire format is camelCase. All request/response JSON keys are camelCase (a
boundary adapter maps them to the server’s internal snake_case), and the
stepCodediscriminator values are PascalCase (GetPhone, …). The generated SDK reflects this — the JSON in this guide is the on-the-wire shape.
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
| Header | Value | When |
|---|---|---|
x-device-id | the server-issued device id from create_device (dvc_…) | every call, from create_device onward |
x-onboarding-prospect | the prospectToken returned by start_onboarding | every onboarding call after start |
- No
Authorization/ bearer token in v1 — nothing is minted untilsecure_device(phase 2). - No request signature in v1. You still register the device’s request-signing
public key at
create_device(it’s part of device creation), but signing is only enforced once a session is minted atsecure_device. See §8 for the open decision on this. x-client-typeis not required by the retail onboarding endpoints (the server resolves the mobile device fromx-device-idalone). Sending it is harmless.- Device binding: the onboarding is bound to the
x-device-idyou started it on. Sending a different device id on later calls returns not-found (404), on purpose — always send the same device id for the whole flow.
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):
offerIdomitted → the backend starts you on the free offer (the user’s real selection isconfirm_offer, §9.2). Pass one only to deep-link a specific offer.countryomitted → defaults toFR(retail is FR-resident-only and there is no country screen; residence is captured atresidence_address).
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:
| Step | stepCode | Payload fields |
|---|---|---|
| Phone | GetPhone | { "phone": "+33…" } to issue, { "code": "123456" } to verify |
GetEmail | { "email": "…" } to issue, { "code": "123456" } to verify | |
| Identity | PersonalIdentity | { "firstName", "lastName", "dateOfBirth": "YYYY-MM-DD" } |
| Address | ResidenceAddress | { "addressLine1", "addressLine2"?, "postalCode", "city" } (country fixed FR) |
| Nationality | Nationality | { "nationalities": ["FR", "US", …] } (multi-select, ISO-3166-1 alpha-2) |
| US person | UsCitizenDeclaration | { "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:
- Issue — submit
{ "stepCode": "GetPhone", "phone": "+33…" }. The response isok: truewithnext.codestillGetPhoneandstatus: "InProgress"— that’s your signal to show the code-entry screen. - Verify — submit
{ "stepCode": "GetPhone", "code": "123456" }. On success the response advances (next.code=GetEmail).
Notes:
- Dev code: in local/dev environments the fixed code
000000is accepted (real SMS/email delivery isn’t wired yet). This does not work on deployed environments. - Enumeration-safety — important: the issue response is identical whether or not the phone/email already belongs to an account. Never branch UI on “already exists”; just proceed to the code screen. (A number/email already in use silently gets a different message in its own inbox and a code that won’t verify.)
- Resend = submit the issue phase again with the same value.
- Wrong / expired code comes back as a field error on
code(see §5), e.g."the code is incorrect","the code has expired","too many attempts, please request a new code".
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" } }
next: the step to render now.nullmeans the gap is empty (ready to submit — phase 2).prefill(on aStep): known values keyed by the step’s own field names, so you can pre-populate a form on resume/edit. Absent when nothing is known.status(enum):InProgress,PendingValidation,ChangesRequested,Validated,Rejected.fieldErrors: map offield name → message. Field names match the payload (phone,email,code,firstName,postalCode,isUsPerson, …). Render inline; the step stays current.
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:
isUsPerson: false→ok: true, andnext.codeisSecureDevice. That’s the part-1/part-2 boundary: continue with the native device securing screens (passcode/Face ID), which perform the mint (§9.1) — from there the flow is authenticated.isUsPerson: true→ok: truewithstatus: "Rejected"andnext: null. Render the ineligibility dead-end (the “Hélas, nous ne pouvons pas vous accueillir” screen). This is terminal.
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:
- Returns the current
OnboardingState(resume atcurrentStep, usingprefillto re-populate), or nullif there’s no in-flight onboarding (start fresh), or- for a Rejected onboarding, the terminal state so you re-render the dead-end.
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).
stepCode | Allowed values |
|---|---|
ProfessionalSituation | Employed, SelfEmployed, Student, Unemployed, PublicServant, CompanyOwner, Retired |
ActivitySector | RealEstate, PublicAdministration, Agriculture, ArtsEntertainment, OtherServices, Construction, SalesTrading, EnergyWater, EducationScience, FinanceInsurance, AccommodationCatering, Industry, InformationCommunication, Technology, HealthSocialServices, Transportation |
SourceOfFunds | Salary, Savings, InheritanceOrFamilySupport, Pension, Annuity, Other |
AnnualIncome | Under25k, From25kTo50k, From50kTo75k, From75kTo100k, From100kTo150k, Over150k |
AccountUtility | MainAccount, SecondaryAccount, Savings |
MonthlyVolume | Under500, From500To1000, From1000To5000, Over5000 |
TransferZones | SepaOnly, OutsideEu |
9.4 PEP (two screens)
{ "stepCode": "PepDeclaration", "isPep": false }— iftrue,category(the public function) is required too.{ "stepCode": "PepEntourage", "hasPepInEntourage": false }
Neither answer rejects the onboarding — a positive declaration is recorded for the compliance review.
9.5 Identity, submission, end state
{ "stepCode": "IdDocPvid" }— « Vérifions votre identité » (Checkout/Ubble; auto-verifies in every environment via the sandbox provider until the real document-capture integration lands — see §9.6).- After
TransferZones,nextisnull(no recap screen on retail): callonboarding/submit_for_review→statusbecomesPendingValidation→ render the « Compte en cours de validation » home. edit_stepreopens an already-completed step to change it.
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:
- dev / staging — fixed
000000OTP code, identity auto-verifies, request signing not enforced. - production — real OTP codes delivered by SMS/email (no
000000), identity still auto-verifies (sandbox provider, until real PVID lands), request signing enforced post-mint.
10. Reference
- Types & paths: the retail OpenAPI (
/retail/openapi.json) and the generated typed client — the source of truth for field-level schemas. - Step codes (
stepCodediscriminator values):GetPhone,GetEmail,PersonalIdentity,ResidenceAddress,Nationality,UsCitizenDeclaration(part 1);SecureDevice,ConfirmOffer,ProfessionalSituation,ActivitySector,SourceOfFunds,AnnualIncome,PepDeclaration,PepEntourage,IdDocPvid,AccountUtility,MonthlyVolume,TransferZones(part 2). Submission goes through the separatesubmit_for_reviewendpoint, notsubmit_step. - Status values:
InProgress,PendingValidation,ChangesRequested,Validated,Rejected. - Engine internals (for the curious): 2. Engine, 4. Lifecycle & Status.
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
- Step — a reusable onboarding requirement from the shared dictionary
([UC-ONB-02]); the engine unit (formerly
Module). Declares what itprovides,requires, and anapplies_to(offer, bag)predicate. - DataPoint — the atomic unit the resolver reasons over; an enum key for one
piece of collected information (
Siret,IdentityVerificationStatus, …). - DataBag — the transient per-onboarding map
DataPoint → BagValue, stored JSONB on the session ([UC-ONB-26]). - Required Step set — the snapshot of the offer’s
offer_required_steprows taken at create ([UC-ONB-02]); see 1. Overview §2. - Satisfied — a Step is satisfied when every DataPoint it
providesis present (and valid) in the bag.
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
- 1. Onboarding Overview — offers drive the Step set.
- 3. Step Dictionary & Sole-Trader Steps — the concrete Steps and DataPoints.
- 4. Onboarding Lifecycle & Status — the status machine the engine runs inside.
- ../../docs/0_use_cases.md — [UC-ONB-02]/[UC-ONB-03]/[UC-ONB-06]/[UC-ONB-13].
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 insteps::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) | provides | requires | Adapter |
|---|---|---|---|
get_email_password | ContactEmail, EmailVerified (+ stages the password credential for the contact-set mint) | — | EmailSender (OTP, stubbed) |
get_phone | PhoneVerified | — | channel-selected: PhoneVerifier (web → Vonage, stubbed; mobile → Infobip silent-auth, deferred) |
personal_info | PersonalFirstName, PersonalLastName, PersonalDateOfBirth, PersonalNationality | — | none |
siret_lookup | Siret, LegalName, LegalForm, CompanyAddress, ApeCode | PersonalLastName | SiretLookupProvider (real INSEE) |
id_doc_pvid | IdentityVerificationStatus | PersonalFirstName, PersonalDateOfBirth | IdentityVerificationProvider |
pep_declaration | IsPep (+ PepCategory, PepFamilyMember, PepCloseAssociate) | PersonalLastName | none |
confirm_offer | OfferConfirmed | (all of the above) | none — reads the offers catalogue for siblings |
recap | RecapConfirmed | (all of the above) | none |
submit_for_review | — (transition Step) | RecapConfirmed | none |
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:
- 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
EmailSenderadapter — 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]). - Verify. The prospect submits the code. On a match within the window the backend
sets
EmailVerifiedin the bag. When that verification completes the contact set (bothEmailVerifiedandPhoneVerified) on an anonymous session, the backend (a) creates a real minimal user + verified email AND phone login-identifiers, (b) mints an authenticated session via theauthenticationcrate — exactly as a successful login would — passwordless: the session records aCredentialKind::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 newuser_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:
- Issue.
{ email, password, confirmPassword }. Validates the password (min length;confirmPasswordmust 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 secretEmailPasswordCredentialHash(never logged, [UC-ONB-35], and stripped from the back-office view); the code is issued exactly asget_email. - Verify.
{ code }. On a match,EmailVerifiedis set; the mint at contact-set completion additionally creates a durablePasswordcredential (from the stored hash) alongside the verified login-identifiers, records the session asCredentialKind::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_pvidcrate,clients::checkouttransport, webhook service, back-office routes, migrations; mTLS + signature keys inparameters/checkout.rs). What remains: the live Ubble Basic credentials +user_journey_id, and reshaping the onboarding seam from the synchronousverify()to start + webhook-driven completion socheckout_pvidplugs 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:
- 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 thePhoneVerifierselected by the request channel ([UC-ONB-33]) (web → Vonage OTP, self-issued hash+expiry in the bag, SMS viamessaging/vonage, 6. Integrations §3 — stubbed; 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. - Verify. On a correct code the backend adds a verified phone login-identifier
to the now-authenticated user and sets
PhoneVerified(and records theVerificationChannel). No new session is minted —get_emailalready 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
- 2. Engine: Resolver, Registry, DataBag — the
Steptrait and resolver semantics. - 6. Integrations & Adapters — the INSEE, email/phone OTP, and PVID adapters.
- uncertainties.md — PVID provider/creds, OTP semantics.
- ../../docs/0_use_cases.md — [UC-ONB-02]/[UC-ONB-04]/[UC-ONB-13]/[UC-ONB-14]/[UC-ONB-28]/[UC-ONB-31]/[UC-ONB-32].
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]). validate →
Validated (freezes the session, stamping validated_at + validated_by,
[UC-ONB-05]); request_changes → ChangesRequested ([UC-ONB-06]); reject →
Rejected, 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
- 2. Engine: Resolver, Registry, DataBag — what runs inside
InProgress. - 5. Validation & Provisioning — UnderReview → Validated, the saga.
- 7. Data Model — the one-in-flight index, status enum.
- ../../docs/0_use_cases.md — [UC-ONB-05]/[UC-ONB-06]/[UC-ONB-07]/[UC-ONB-15]/[UC-ONB-23]/[UC-ONB-25]/[UC-ONB-28]/[UC-ONB-29]/[UC-ONB-30]/[UC-ONB-31]/[UC-ONB-32].
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
- reason code), and the session moves to
ChangesRequested. Everything else stays in the DataBag; only the unmet Steps are asked again ([UC-ONB-06]). This is the same re-entry path as any resume ([UC-ONB-06], 4. Lifecycle §2.5).
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
- 4. Onboarding Lifecycle & Status — the status transitions around validation.
- 3. Step Dictionary & Sole-Trader Steps — the Steps that must be satisfied first.
- 7. Data Model —
OnboardingSession(incl. validation freeze fields),OfficerComment. - uncertainties.md — saga compensation ([ONB-U4]).
- ../../docs/0_use_cases.md — [UC-ONB-05]/[UC-ONB-06]/[UC-ONB-07]/[UC-ONB-25]/[UC-ONB-27]/[UC-ONB-28]/[UC-ONB-29]/[UC-ONB-31].
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.
- Trait reused:
clients::insee::InseeApi(get_company_data). - Credentials:
PARAMETERS.insee.api_key— already exist in the repo (InseeClientreads it; headerX-INSEE-Api-Key-Integration). No new env work. - Failure handling: a not-found / inactive SIRET returns
StepError::ValidationFailed(Step not advanced); a transport error returnsStepError::ExternalCallFailedand is retried per the activity policy.
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.
EmailSender(get_email) — delivers the email OTP. Stubbed for now (no email transport exists incoreyet; themessagingcrate is SMS-only). State in bag:EmailVerifiedplus the transientEmailOtpChallengeHash/EmailOtpChallengeExpiresAt.PhoneVerifier(get_phone) — channel-selected ([UC-ONB-33]): web → Vonage OTP delivered over SMS, reusing the existingmessaging/vonagesend path (stubbed for now); mobile → Infobip silent network authentication — a typed seam, deferred (only the env parameters exist today, no adapter). State in bag:PhoneVerified,VerificationChannel, plus the transient phone-OTP hash+expiry.- Failure handling: send failure ⇒
StepError::ExternalCallFailed(retry); wrong/expired code ⇒StepError::ValidationFailed(Step not advanced).
✅ 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_emailandget_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:
- a real provider backed by Ubble (the
checkout_pviddomain crate over theclients::checkouttransport), and - a config-gated sandbox implementation used in dev/test and behind a flag, which returns deterministic verification outcomes without any external call.
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_pvidcrate (stores, use-cases, status machine), theclients::checkouttransport (mTLS, v2 DTOs,Cko-Signatureverifier), thecheckout_webhookservice, back-office routes, the mirror migrations, and the mTLS cert/key + webhook signature keys insrc/env/src/parameters/checkout.rs. Still open: the live Ubble Basic credentials (client_id/client_secret,Secret::todo()) and the Certified/PVIDuser_journey_id(empty) — pending from the Ubble account manager; and the onboarding seam, whoseRealIdentityProviderstillbail!s — the synchronousverify()must be reshaped to start + webhook-driven completion beforecheckout_pvidplugs 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
| Adapter | Client | Credentials | Status |
|---|---|---|---|
SiretLookupProvider | clients::insee::InseeApi | PARAMETERS.insee.api_key | exists |
EmailSender | none (email OTP) | none | stubbed (no email transport in core yet) |
PhoneVerifier (web) | messaging / vonage | existing messaging creds | stubbed |
PhoneVerifier (mobile) | Infobip silent-auth | PARAMETERS.infobip.* (params only) | deferred |
IdentityVerificationProvider | Ubble (Checkout.com) — clients::checkout + checkout_pvid | mTLS cert/key + signature keys set in parameters/checkout.rs; Basic client_id/client_secret + user_journey_id pending | mostly built, launch-blocking until creds + onboarding wire land (PVID integration §12) |
6. Related documents
- 3. Step Dictionary & Sole-Trader Steps — which Step uses which adapter.
- 8. Architecture — where adapters sit in the DDD layout; async result write-back.
- uncertainties.md — PVID provider/creds, OTP semantics.
- ../../docs/0_use_cases.md — [UC-ONB-04]/[UC-ONB-14].
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).
| Field | Type | Notes |
|---|---|---|
id | onb_… | Prefixed, time-sortable. |
offer_id | text | The chosen offer (owned by offers). One offer per onboarding ([UC-ONB-02]). |
segment | Segment (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_id | text, nullable | Authenticated owner. Exactly one of user_id / prospect_token is set ([UC-ONB-28]). ≤ 1 in-flight per user ([UC-ONB-03]). |
prospect_token | text, nullable | Anonymous 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]). |
status | OnboardingStatus | See 4. Lifecycle. |
data_bag | jsonb | DataPoint → BagValue; transient working state ([UC-ONB-26]). |
required_steps | text[] | Snapshot of the offer’s offer_required_step set at create; immutable for the session ([UC-ONB-02]/[UC-ONB-23]). |
unmet_steps | text[] | Officer-injected remediation; re-surfaced by the resolver ([UC-ONB-06]). |
officer_comments | jsonb | OfficerComment[] (§4). |
current_cursor | text, nullable | Last resolver output, for quick resume ([UC-ONB-03]). |
started_at, last_active_at | timestamptz | last_active_at doubles as updated-at. |
submitted_at, validated_at | timestamptz, nullable | Lifecycle stamps; validated_at is the validation-freeze time. |
validated_by | text, nullable | Operator id of the validating compliance officer. Some iff Validated ([UC-ONB-05]). |
expires_at | timestamptz | TTL 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).
| Field | Type | Notes |
|---|---|---|
comment_id | text | Stable id within the array. |
steps | text[] | The Step ids this comment marks unmet ([UC-ONB-06]). |
reason_code | text | Mandatory BO reason code ([UC-BO-10]). |
body | text | Free-text officer note shown to the customer. |
officer_id | text | The acting officer (audit). |
created_at | timestamptz |
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.
| Field | Type | Notes |
|---|---|---|
id | onbev_… | Prefixed, time-sortable. |
onboarding_id | text | FK to onboarding_session. Indexed for the back-office read. |
seq | bigint | Per-onboarding monotonic order (max(seq)+1 at append). |
kind | text | Event kind — step_submitted this round; the enum grows (e.g. step_failed, resumed). |
occurred_at | timestamptz | When the event happened. |
actor | text | Who acted — prospect:<token-hash>, a user_id, or an officer_id. |
server_meta | jsonb | Authoritative, 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_meta | jsonb | Untrusted, 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
- 4. Onboarding Lifecycle & Status — the status enum and transitions.
- 9. Onboarding Events & Risk Signals — the event log, client-meta channel, PEP step, and derived global signals in full.
- 5. Validation & Provisioning — the validation freeze.
- 8. Architecture — stores and migrations.
- ../../docs/0_use_cases.md — [UC-ONB-03]/[UC-ONB-06]/[UC-ONB-15]/[UC-ONB-26]/[UC-ONB-27]/[UC-ONB-28]/[UC-ONB-29]/[UC-ONB-30]/[UC-ONB-31]/[UC-ONB-32].
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
| Store | Key methods |
|---|---|
session_store | create, 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 case | Effect |
|---|---|
create_onboarding | bind 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_step | run resolver; return next StepId + prefill + status ([UC-ONB-03]) |
submit_step | validate, step.on_complete, persist bag, resolve next ([UC-ONB-02]) |
submit_for_review | guard resolver == None; → Submitted |
abandon_onboarding | → Abandoned |
get_data_bag | rate-limited, redacted preview |
Back-office (called from back_office_api, [UC-ONB-05]/[UC-BO-06]/[UC-BO-07]):
| Use case | Effect |
|---|---|
validate_onboarding | guard Submitted/UnderReview + no gap; promote anonymous owner; freeze session (stamp validated_at + validated_by); → Validated ([UC-ONB-05]/[UC-ONB-27]) |
request_changes | mark Step(s) unmet + OfficerComment; → ChangesRequested ([UC-ONB-06]) |
reject_onboarding | full 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.
- Expiry sweep (daily scheduler job) — batches sessions past
expires_atin a non-terminal state and archives them (Expired, emitOnboardingExpired), retaining the row ([UC-ONB-15]). Each session runs in its own transaction, so a per-session failure is rolled back, logged, and skipped without aborting the batch. on_identity_resultrule / activity — the PVID provider’s async result is written back to the bag (IdentityVerificationStatus), then the resolver advances on the nextget_current_step(6. Integrations §4).
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
- 2. Engine: Resolver, Registry, DataBag
- 5. Validation & Provisioning
- 7. Data Model
- ../plan.md — the build sequence and full module layout.
- ../../docs/0_use_cases.md — [UC-ONB-01]/[UC-ONB-02]/[UC-ONB-03]/[UC-ONB-05]/[UC-ONB-28]/[UC-ONB-29]/[UC-ONB-30]/[UC-ONB-31]/[UC-ONB-32]/[UC-BO-06]/[UC-BO-09]/[UC-BO-10].
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:
| Field | Source |
|---|---|
ip, city, region, country, latitude, longitude, asn | the 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_type | the existing device domain — web = the device_id httpOnly cookie, mobile = the x-device-id header (authentication::…::extract::extract_device_info). |
user_agent | request header (bounded length). |
step_code | the step being submitted. |
saved_data_points | the 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:
- Signals:
view(the view/step code),duration_ms(dwell time on the view),tab_switches(document visibility changes away from the tab),paste_count,copy_count, focus/blur counts, and first-interaction latency. The set is open — the client may add fields; the backend stores the decoded object verbatim inclient_meta. - Transport: packed to JSON and base64-encoded into the
x-onb-metarequest header. This is obfuscation, not security — it keeps casual inspection out and namespaces the payload, but it is deliberately unsigned this round. A determined actor can forge it. - Trust:
client_metais advisory only. It never gates a decision, is stored beside (not merged into) the authoritative server meta, and is treated as possibly-absent / possibly-forged by any reader. When the future scorer weights it, that weighting must assume adversarial input.
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:
IsPep(bool) — always;PepCategory(text) — the public function / category held, required whenIsPepis true;PepFamilyMember(bool) — family member of a PEP;PepCloseAssociate(bool) — known close associate of a PEP.
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):
- Onboarding events — the raw append-only timeline: per event, timestamp, step, DataPoints written, decoded server + client meta. Read-only.
- PEP — the four declaration fields above.
- Screening (out of scope) — a static placeholder marking sanctions/watchlist screening as a not-yet-wired separate regime ([UC-REG-02]).
- Global signals (derived) — see §6.
6. Derived global signals
Computed by aggregating the onboarding’s events at read time — not stored:
| Signal | Derivation |
|---|---|
| Total duration | last event occurred_at − started_at (or first event). |
| Devices used | distinct server_meta.device_id (+ type) across events. |
| Device changed | more than one distinct device_id. |
| Device reuse + links | for 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 array | distinct server_meta.ip. |
| Browser array | distinct server_meta.user_agent. |
| Country array + flags | distinct server_meta.country (ISO code → flag rendered by the front end). |
| Behavioural roll-up | sums/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
- 3. Step Dictionary & Sole-Trader Steps —
pep_declarationStep (§2.6) and DataPoints. - 7. Data Model — the
onboarding_eventtable (§5). - 8. Architecture — stores, use cases, and the API surface.
- ../../docs/0_use_cases.md — [UC-ONB-08]/[UC-ONB-35]/[UC-ONB-36]/[UC-BO-12]/[UC-REG-02]/[UC-REG-08].
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.
- Class:
compliance-policy/security· Owner: Security + Compliance + Backend · Due: Before anonymous onboarding go-live · Launch-blocking: no (a soft message can default until the policy is set) - Evidence: 4. Lifecycle §2.8, 8. Architecture §5, ../../docs/0_use_cases.md ([UC-ONB-32]/[UC-REG-02])
- Status: OPEN
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
- 1. Subscriptions Overview — scope, the holder↔offer relation, the one-active-subscription-per-IBAN invariant, and the boundaries with offers / customer_billing / core_banking.
- 2. Data Model — the
Subscriptionaggregate, the offer/module snapshot, customer-specific modifiers, filiation; ER diagram; invariants → UC.
Lifecycle & changes
- 3. Lifecycles and State Machines — the three distinct lifecycles (offer / subscription / account) and the subscription status machine (UC-SUB-33).
- 4. Offer Changes and Proration — close-then-open; upgrade (immediate when collectable), downgrade (perks to boundary), lateral; netting (UC-SUB-06/07/08, UC-BIL-08).
- 5. Bundles, Merge and Explosion — bundle = one subscription many modules; dismantle/explosion and the reverse merge (UC-SUB-04/05/21).
- 6. Suspension, Closure and Death — AML-only suspension, async closure wind-down, holder-death manual termination (UC-SUB-19/22/27).
Architecture & open items
- 7. Architecture — the intended DDD layers, segment-neutrality,
the event boundary with
customer_billing, and the closure Temporal workflow. - Uncertainties — the active, classified register of open subscription items (suspension max-duration, negative-balance cap/window, framework-termination notice, annual-proration regime), each with owner, milestone, launch-blocking flag, and evidence link.
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:
- Single individual account —
S1 = (P, Essential, {current_account, card}, active)(UC-SUB-01). - Individual + shared — two subscriptions; a shared account is the primary holder’s account with a participant, not a co-owned account (UC-SUB-02).
- Bundle —
S = (P, Valentine bundle, {current, shared}, active): one subscription, one price (UC-SUB-04). - Savings module — a livret/ASV/PER subscription provisions/links an
investmentcontract, is free (no billing line), and earns Green-Got partner commission (UC-SUB-15).
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).
| Owns | References (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-down | the Offer / Module / Step catalogue, eligibility, versioning (offers) |
| The offer/module snapshot + customer modifier declarations | identity / KYC (physical_person); AML decisions (compliance) |
Filiation lineage (previous_subscription_id) | savings / ASV / PER contracts (investment + partners) |
| Lifecycle events that trigger billing | invoices / 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:
- Billing cadence —
Monthly | Annual. - Period — the span the current charge covers.
- Anniversary — the day the period renews; the subscription’s own anchor (UC-BIL-02).
- Period boundary — the instant one period ends and the next begins.
- Anchor drift — an upgrade resets the anchor to the switch date (close-then-open, UC-BIL-08), so the anniversary intentionally drifts.
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 — the aggregate, snapshot, modifiers, filiation.
- 3. Lifecycles and State Machines — the three lifecycles and the status machine.
- 4. Offer Changes and Proration — upgrade / downgrade / lateral.
- commercial_domain/docs/0_use_cases.md — the cross-domain spine (§4 UC-SUB-*).
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.
| Field | Type | Notes |
|---|---|---|
id | “sub_…” | Prefixed, time-sortable. |
holder_id | string | Reference to the holder (physical_person / organisation); the holder owns the account + money (UC-ONB-12). |
account_ids | Vec<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_snapshot | OfferSnapshot | Frozen offer_version + module_versions + price_snapshot (UC-SUB-32). See §2. |
cadence | Monthly | Annual | Billing cadence (UC-SUB-31). |
anchor | timestamp | The 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). |
status | SubscriptionStatus | See 3. Lifecycles (UC-SUB-33). |
modifiers[] | SubscriptionModifier | Customer-specific pricing overrides, applied at invoicing (UC-SUB-29). See §3. |
previous_subscription_id | “sub_…”, nullable | Filiation to the predecessor a close-then-open created (UC-SUB-32). See §4. |
pending_change | PendingChange, nullable | At most one, and only a downgrade (UC-SUB-10). See 4. Offer Changes. |
created_at, expired_at | timestamps | expired_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).
| Field | Type | Notes |
|---|---|---|
offer_id | string | The offer the subscription is on. |
offer_version | u32 | The frozen immutable offer version (UC-OFF-06). |
module_versions[] | ModuleVersion | Each referenced module’s frozen version (payment account, shared account, card sub-module, livret, ASV, PER). |
price_snapshot | i64 (cents) | The catalogue price delivered (UC-OFF-03); promos are intrinsic to the offer, not stacked at runtime (Appendix A). |
fee_schedule_version | u32 | The 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.
| Modifier | Effect | Set / 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 counter | Decrements at each cycle; increments from parrainage (UC-BIL-20), compensation gestures (UC-BIL-07), or promos (UC-BIL-17). | billing events |
| Promo-code discount | A 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).
- An offer change produces one predecessor link (closed → opened) (UC-SUB-06 / UC-SUB-07).
- A bundle explosion produces several successors, each surviving module’s fallback subscription linking back to the bundle (UC-SUB-05).
- A bundle merge produces one successor linking back from several closed unit subscriptions (UC-SUB-21).
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
- 1. Subscriptions Overview — boundaries and invariants.
- 3. Lifecycles and State Machines — the status field’s machine.
- 4. Offer Changes and Proration — how filiation is produced and the snapshot recomputed.
- commercial_domain/docs/0_use_cases.md — §4 UC-SUB-*.
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).
| Lifecycle | Owner | States | Notes |
|---|---|---|---|
| Offer | offers | draft → active (within [start, end]) → retired | Immutable versions (UC-OFF-06); retirement does not close live subscriptions (UC-SUB-17). |
| Subscription | subscriptions | pending → active → (suspended / arrears) → expired/closed | An offer change ends one and opens another (UC-SUB-12). |
| Account | core_banking | active → suspended (AML) / closing → archived | Can outlive its subscription (closing wind-down); tier-agnostic (UC-SUB-16, UC-SUB-19). |
Invariant — consequences of separation:
- A subscription ending does not instantly close the account (UC-SUB-19, UC-SUB-33).
- A retired offer does not close live subscriptions; a grandfathered subscription continues unchanged and migrates only on an explicit event (UC-SUB-17).
- Suspension acts on the account, not the subscription’s commercial state — billing cadence and the offer relation persist while spending is blocked (UC-SUB-22, UC-SUB-33).
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:
Pending→Activeis driven by activation: the onboarding orchestrator provisions the concrete instances (person + payment account + card, or organisation + business account); the generic crates only record the resulting links (UC-SUB-18).Active→Expiredis always close-then-open: an offer change or a boundary cancellation expires the generation and (for a change) opens the successor, linked by filiation (UC-SUB-12, UC-SUB-32).Arrearsis commercial, not AML. A failed fee charge keeps the account open and the subscription active; the fee invoice stays open/partially-paid → overdue and runs the dunning cadence (−1d, +1d, +1w, +1m, +2m, then stop). It restricts optional paid actions but never blocks spending and never triggers suspension (UC-SUB-14).Suspendedis reached only via the AML/fraud/security path; lifting is privileged (managers/compliance) (UC-SUB-22). Detailed in 6. Suspension, Closure and Death.ExpiredvsClosed.Expiredmarks a superseded generation (its successor carries the relation forward); its snapshot and filiation are retained.Closedis the terminal state of the relation itself; the account may still run an async wind-down after the subscription is closed (UC-SUB-19, UC-SUB-33).
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
- 2. Data Model — the
statusfield and filiation. - 4. Offer Changes and Proration — the
Active → Expiredtransitions in detail. - 6. Suspension, Closure and Death —
Suspended/Closed. - commercial_domain/docs/0_use_cases.md — §4 UC-SUB-*.
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:
- the full price of the new subscription for its first period;
- 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).
- Monthly → Annual — immediate (commitment upgrade).
- Annual → Monthly — at the end of the already-paid year.
- Early exit — forfeits the discount, never access or money.
The annual price is realised by one of two regimes — prepay-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
- 3. Lifecycles and State Machines — the
Active → Expiredtransitions. - 5. Bundles, Merge and Explosion — multi-module changes.
- 6. Suspension, Closure and Death — termination and refunds.
- commercial_domain/docs/0_use_cases.md — UC-SUB-06/07/08/10, UC-BIL-08/09.
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
- 4. Offer Changes and Proration — close-then-open and boundary mechanics shared by dismantle/merge.
- 2. Data Model — filiation produced by explosion (fan-out) and merge (fan-in).
- commercial_domain/docs/0_use_cases.md — UC-SUB-04/05/21, UC-SUB-12/13.
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:
- payment means are blocked (no spending / transfers);
- the user can still consult and download account details and statements.
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:
- no new activity — incoming SEPA credits bounce / are returned to sender, not accepted;
- ongoing transactions, disputes, scheme presentments, legal holds, and investigations must settle before the account can be archived.
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:
| Path | Effect | Refund |
|---|---|---|
| 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
- 3. Lifecycles and State Machines — the
Suspended/Closedstates and the account lifecycle. - 4. Offer Changes and Proration — refund machinery and annual proration.
- uncertainties.md — SUB-22 / SUB-24 / SUB-30 open items.
- commercial_domain/docs/0_use_cases.md — UC-SUB-19/22/23/24/27/30.
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.
| Layer | Contents | Examples |
|---|---|---|
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
- plan.md — concrete module layout, intended types, build sequence.
- 1. Subscriptions Overview — domain boundaries.
- 3. Lifecycles and State Machines — the state machines the domain layer implements.
- commercial_domain/docs/0_use_cases.md — §4 UC-SUB-* and Appendix A.
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).
- Class:
legal/product-decision· Owner: Claude (research) → Compliance / Product · Due: Before suspension goes live · Launch-blocking: yes - Evidence: 6. Suspension, Closure and Death §1, UC-SUB-22 (⚠️ OPEN: the max-duration value)
- Status: RESEARCH PENDING (Claude)
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).
- Class:
product-decision(withcustomer_billing) · Owner: Claude (research) → Product / Billing · Due: Before suspension goes live · Launch-blocking: yes - Evidence: 6. Suspension, Closure and Death §1, UC-SUB-22
- Status: RESEARCH PENDING (Claude)
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).
- Class:
legal/product-decision· Owner: Claude (research) → Risk / Compliance · Due: Before card spending can create negative positions at scale · Launch-blocking: yes - Evidence: 6. Suspension, Closure and Death §5, UC-SUB-24 (⚠️ OPEN: cap value and recovery-window
N) - Status: RESEARCH PENDING (Claude)
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).
- Class:
legal/product-decision· Owner: Claude (research) → Legal / Product · Due: Before self-service termination goes live · Launch-blocking: yes - Evidence: 6. Suspension, Closure and Death §3, UC-SUB-30
- Status: RESEARCH PENDING (Claude)
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).
- Class:
product-decision(withcustomer_billing) · Owner: Claude (research) → Product / Billing · Due: Before annual cadence ships · Launch-blocking: yes (for annual offers) - Evidence: 4. Offer Changes and Proration §7, UC-SUB-09
- Status: RESEARCH PENDING (Claude)
2. Resolved decisions (awaiting port into canonical docs)
None yet. Decisions taken in this round are already reflected in the canonical docs:
- Suspension is AML-only; unpaid fees produce a commercial arrears state, never suspension — documented in 3. Lifecycles §2 and 6 §1 (UC-SUB-14, UC-SUB-22).
- Every offer change is close-then-open with filiation; the snapshot is never mutated — documented in 2. Data Model §2/§4 and 4 §1 (UC-SUB-12, UC-SUB-32).
- Upgrades are immediate-when-collectable with no deferred-upgrade state; only a downgrade can sit pending — documented in 4 §3/§6 (UC-SUB-06, UC-SUB-10).
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
- 3. Lifecycles and State Machines — the status machine the suspension/arrears decisions live on.
- 4. Offer Changes and Proration — the annual-proration regime (SUB-09).
- 6. Suspension, Closure and Death — SUB-22 / SUB-24 / SUB-30 home docs.
- 7. Architecture §6 — where the tunables land in code.
- commercial_domain/docs/0_use_cases.md — §4 UC-SUB-*.
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:
| Field | Information |
|---|---|
| Title | Mr |
| Last name | GANOVELLI |
| First name | Andréa |
| Title/function | Chief Executive Officer of Green-Got |
| Telephone no. | 06 01 19 25 62 |
| andrea@green-got.com |
Person to contact for any question about the dossier:
| Field | Information |
|---|---|
| Title | Ms |
| Last name | LARRE |
| First name | Lola |
| Title/function | Associate Director of the firm B-Part |
| Telephone no. | 06 73 70 55 83 |
| l.larre@bpart-consulting.com |
Signature of the person, legal representative of the company:
| Field | Information |
|---|---|
| Last name | GANOVELLI |
| First name | Andréa |
| Function | Chief Executive Officer |
| Date | 10/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:
- CMA (Crédit Mutuel Arkéa), a Credit Institution that will be the SEPA lead bank and will provide the connection to the SEPA scheme and the CSMs (Clearing and Settlement Mechanisms).
- ABS (Arkéa Banking Services), a Credit Institution that distributes CMA’s B2B products and manages the commercial relationship with DOMINO SAS.
- AGC (Agence Grand Compte), a shared entity that operates payments and the technical infrastructure.
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
| Field | Information |
|---|---|
| Corporate name | DOMINO |
| Trade name if different | Geen-Got |
| Siren | 883 981 763 |
| Legal form | Société par actions simplifiée |
| Registered office address | 20 bis rue Louis Philippe |
| Postal code | 92200 |
| City | Neuilly-sur-Seine |
| Country | France |
| Telephone no. | 06 01 19 25 62 |
| contact@green-got.com | |
| Website | https://green-got.com |
Information Relating to Share Capital
| Field | Information |
|---|---|
| Amount of share capital in EUR | EUR 24,809.70 |
| Amount of capital to be paid up in EUR | EUR 13.5M fundraising scheduled for 2024 (year N-1 of DOMINO SAS’s BP) |
| Effective date | After 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:
| Shareholder | Share of capital / voting rights |
|---|---|
| Maud CAILLAUX | 19.84% |
| Andréa GANOVELLI | 19.84% |
| Fabien HUET | 16.82% |
| Pale Blue Dot | 9.85% |
| Crowdcube Nominees Limited | 5.42% |
| David MOISON | 4.62% |
| Romain DURAND | 2.74% |
| Area Climate and Frontier Fund | 2.72% |
| Various natural / legal persons | 18.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 name | Share of capital in % | Share of voting rights in % | |
|---|---|---|---|
| Shareholder 1 | Maud CAILLAUX | 19.84 | 19.84 |
| Shareholder 2 | Andréa GANOVELLI | 19.84 | 19.84 |
| Shareholder 3 | Fabien HUET | 16.82 | 16.82 |
The following are provided in the annexes:
- For Ms Maud CAILLAUX:
- 2.1.1 Qualified shareholder questionnaire of Ms CAILLAUX.
- 2.1.2 CV of Ms CAILLAUX.
- 2.1.3 Identity document of Ms CAILLAUX.
- 2.1.4 Non-conviction declaration of Ms CAILLAUX.
- 2.1.5 Criminal record extract of Ms CAILLAUX.
- Mr Andréa GANOVELLI:
- 2.2.1 Qualified shareholder questionnaire of Mr GANOVELLI.
- 2.2.2 CV of Mr GANOVELLI.
- 2.2.3 Identity document of Mr GANOVELLI.
- 2.2.4 Non-conviction declaration of Mr GANOVELLI.
- 2.2.5 Criminal record extract of Mr GANOVELLI.
- Mr Fabien HUET:
- 2.3.1 Qualified shareholder questionnaire of Mr HUET.
- 2.3.2 CV of Mr HUET.
- 2.3.3 Identity document of Mr HUET.
- 2.3.4 Non-conviction declaration of Mr HUET.
- 2.3.5 Criminal record extract of Mr HUET.
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:
- Karl Hampus Jakobsson.
- Heidi Lindvall.
- Joel Larsson.
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:
- Ms MAUD CAILLAUX:
- Ms CAILLAUX holds a Diplôme d’Études Supérieures en Management (DESMA), following the Grande École program of Grenoble École de Management, a Master’s degree in international trade from Grenoble École de Management, and a Master’s degree in Entrepreneurship from Grenoble École de Management.
- Ms CAILLAUX obtained her AMF certification on 24/05/2023 and her professional background is as follows:
- International Business Developer at CIC, in New York, USA.
- Consultant at Capgemini Invent, in Paris.
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.
- Mr Andréa GANOVELLI:
- Mr GANOVELLI holds a Diplôme d’Études Supérieures en Management (DESMA) from Grenoble École de Management, as well as a Diplôme Universitaire et Technologique (DUT) in Business and Administration Management (GEA) with a specialization in Finance and Accounting.
- Mr GANOVELLI obtained his AMF certification on 14/04/2023 and his professional background is as follows:
- Financial Analyst at Pernod Ricard Ireland (Irish Distillers), in Dublin, Ireland.
- Senior Financial Analyst at Pernod Ricard USA, in New York, USA.
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:
- Mr Andréa GANOVELLI, Chief Executive Officer and Effective Manager of DOMINO SAS.
- Mr Kaïss BOURSRY, Head of Risk & Internal Control of DOMINO SAS.
- Mr Pierre-Manuel SROCZYNSKI, compliance, risk, and finance consultant at Somerset Advisory and former Director of Compliance and Permanent Control at La Banque Postale.
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
| Field | Information |
|---|---|
| Title | Mr |
| Usual name | GANOVELLI |
| Family name | GANOVELLI |
| First name | Andréa |
| Other first names | Yannick, Adrien |
| Date of birth | 27/06/1992 |
| Country of birth | France |
| Municipality of birth | Nice |
| Postal code of municipality of birth | 06100 |
| Nationality | French |
| Other nationality | N/A |
| Address | 33 rue des Deux ponts |
| Postal code | 75004 |
| City | Paris |
| Country | France |
| Function | Member of the Supervisory Board |
| Start date | Upon obtaining the license |
Kaïss BOUSRY, Member of the Supervisory Board
| Field | Information |
|---|---|
| Title | Mr |
| Usual name | BOUSRY |
| Family name | BOUSRY |
| First name | Kaïss |
| Other first names | N/A |
| Date of birth | 22/08/1992 |
| Country of birth | COMOROS |
| Municipality of birth | MORONI |
| Postal code of municipality of birth | 99 |
| Nationality | French |
| Other nationality | N/A |
| Address | 8 RUE JEAN DE LA FONTAINE |
| Postal code | 44000 |
| City | NANTES |
| Country | FRANCE |
| Function | Member of the Supervisory Board |
| Start date | Upon obtaining the license |
Pierre-Manuel SCROZYNSKI, Member of the Supervisory Board
| Field | Information |
|---|---|
| Title | Mr |
| Usual name | SCROZYNSKI |
| Family name | SCROZYNSKI |
| First name | Pierre-Manuel |
| Other first names | N/A |
| Date of birth | 31/07/1962 |
| Country of birth | France |
| Municipality of birth | Briey |
| Postal code of municipality of birth | 54150 |
| Nationality | French |
| Other nationality | N/A |
| Address | 27C rue des fours à Chaux |
| Postal code | 63118 |
| City | Cebezat |
| Country | France |
| Function | Member of the Supervisory Board |
| Start date | Upon obtaining the license |
Committees
To date, in accordance with the Shareholders’ Agreement, DOMINO SAS has established a Strategic Committee composed of the following members:
- Ms Maud CAILLAUX, President of Green-Got.
- Mr Andréa GANOVELLI, Chief Executive Officer of Green-Got.
- Ms Heidi LINDVALL, General Partner of Pale Blue Dot.
This Committee meets whenever a strategic decision must be adopted, and at least three times per year. The strategic decisions in question include:
- The issuance of options, free shares, or warrants giving the right to acquire or subscribe for shares of the Company or any Subsidiary, and any increase in the Company’s share capital, whether immediate or deferred.
- Any change to the terms and conditions of the Seed Shares and any issuance of new Securities or change to existing Securities that has a negative impact on the terms and conditions of the Seed Shares.
- Any capital reduction, repayment, repurchase, subdivision or consolidation of shares, creation of classes of shares, or modification of rights attached to securities, provided that prior consent is not unreasonably withheld.
- The repayment of all securities of the company.
- The implementation of any share-based incentive plan, as well as the implementation or modification of the 2023 ESOP plan and the increase of any existing share-based incentive plan, and the determination of the conditions and list of beneficiaries of any such incentive plan.
- Any acquisition of securities from another company or any business combination, including joint ventures.
- Any material modification of the bylaws, other than the change of the Company’s registered office in France.
- The declaration or payment of a dividend, including interim dividends, or another distribution.
- The authorization, issuance, or creation of a digital token, cryptocurrency, other blockchain-based asset, or similar asset.
- Any merger, consolidation, acquisition, or similar transaction resulting in a change of Control of the Company.
- The listing (initial listing) of the Securities of the Company or a Subsidiary on a regulated or unregulated market or stock exchange in France or abroad, including preparatory steps and/or press release.
- The dissolution, receivership, or liquidation of the Company or a Subsidiary, the opening of voluntary bankruptcy proceedings, or any other proceeding having a similar effect, unless the opening of such proceeding is required under applicable law.
- Any agreement or commitment with a shareholder or a member of a shareholder’s family, or an entity directly or indirectly controlled by such person, or in which such person has a direct or indirect interest, and more generally any agreement referred to in Article L.227-10 of the French Commercial Code other than on arm’s-length terms and in the ordinary course of business.
- Any transfer, contribution, pledge, or disposal of assets of the Company or one of its Subsidiaries, including but not limited to intellectual property rights, exceeding an individual aggregate amount of EUR 100,000 over a 12-month period, or the transfer of ownership, for any reason, of intellectual property rights necessary for the continuation and development of the activities of the Company or one of its Subsidiaries.
- The incurrence of debt or guarantees, or the entry into any agreement other than those specifically provided for in the annual budget for a commitment of the Company exceeding EUR 100,000.
- Any incorporation, liquidation, merger, or reorganization of a subsidiary; any acquisition of equity interests; any opening or closing of offices, branches, or establishments by the company or a subsidiary.
- Any material change in the strategic direction of the business.
- Any decision to waive the non-compete clause of one of the founders.
As part of this Payment Institution license application, DOMINO SAS will establish the following non-statutory committees:
- A Management Committee:
- Meeting every 2 months.
- Composed of the following individuals:
- Ms Maud CAILLAUX.
- Mr Andréa GANOVELLI.
- Mr Fabien HUET, Chief Technology Officer.
- Mr Kaiss BOUSRY, Head of Risk and Internal Control.
- Whose responsibilities include:
- Assisting the Strategic Committee in determining DOMINO SAS’s major strategic orientations.
- Determining operational priorities to carry out the strategy decided by the Strategic Committee.
- Executing the company’s strategy.
- An Audit and Risk Committee:
- Meeting every month.
- Composed of the following individuals:
- Mr Andréa GANOVELLI.
- Mr Fabien HUET.
- Mr Kaiss BOUSRY.
- The Head of Compliance, currently being recruited.
- Whose responsibilities include:
- Assisting the Supervisory Board in determining the branch’s risk appetite, particularly for AML/CFT.
- Assisting the Supervisory Board in reviewing, approving, and adopting the AML/CFT Policy and the ML/TF Risk Classification, as well as assessing their effectiveness and the corrective measures adopted.
- Taking a position on the risks incurred by Green-Got, particularly AML/CFT risks.
- Validating onboarding of certain high-risk Clients.
- Approving the Annual Activity Report.
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:
- Opening of a Payment Account in EUR with FR or BE IBAN.
- Issuance of a Mastercard payment Card (debit) enabling Payment Transactions and ATM withdrawals.
- Acquisition / issuance of SEPA SCT Transfers, including recurring and scheduled transfers.
- Acquisition and revocation of Direct Debit orders.
Via their Client Area, accessible from the Green-Got Mobile Application or the Website https://green-got.com, Clients can:
- View the balance of their Payment Account.
- Obtain information on contactless payment limits, withdrawal limits, and monthly and daily spending limits.
- Manage beneficiaries for SEPA Transfers.
- Contact customer service by chat.
- View the PIN code of the Mastercard payment Card, oppose it, temporarily block it, and order a new one.
- Add the Card to Apple Pay and Google Pay for mobile payment.
In addition, DOMINO SAS also offers the following features:
- A CO2 calculator to determine the quantities of CO2 emitted during Card Payment Transactions, enabling its Clients to steer their consumption toward more sustainable alternatives.
- A “money jar” feature allowing the Client to set money aside in a dedicated pot. This feature does not generate interest and is in no way a savings product, but rather a budget management aid.
- A “joint account” feature allowing a Payment Account to be opened in the name of several Clients.
- Real-time monitoring of the impact obtained by supporting supported associations.
- Categorization of the expenses made by the Client.
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:
- 18,565 Clients in France, for transaction amounts of EUR 44M per month in November 2023.
- 964 Clients in Belgium, for transaction amounts of EUR 1M per month in November 2023.
DOMINO SAS was able to finance its development by combining the following financial levers:
- Non-dilutive financing for a cumulative total amount of EUR 1.690M in loans, including:
- 2021: EUR 90K from Wilco (honor loan).
- 2022: EUR 400K from BPI.
- 2023: EUR 1.2M from BPI.
- Grants for a cumulative total amount of EUR 255K obtained from various ecosystem players:
- 2021: EUR 30K in grants (BPI Innov’Up).
- 2022: EUR 25K in grants (Mastercard challenge).
- 2022: EUR 150K in grants (BPI R&D).
- 2022: EUR 50K in support (Mastercard).
- Several fundraising rounds, for a cumulative total amount of EUR 6.35M since DOMINO SAS was created:
- 2020: EUR 350K (capital increase).
- 2021: EUR 185K (BSA-AIR 1).
- 2022: EUR 800K (BSA-AIR 2).
- 2023: EUR 5M, broken down as follows:
- April 2023: Capital entry of investors including PALE BLUE DOT, up to 15.43% of the capital including 9.85% for PALE BLUE DOT, an investment company specialized in “Climate Tech” at “pre seed” or “seed” stage, as well as other shareholders: EUR 3.56M, including EUR 2M from PALE BLUE DOT.
- June 2023: Capital entry of investors including a “multitude” of small shareholders, 1,384 Natural Persons and Legal Persons grouped around a “nominee”, up to 6.44% of the capital, including 5.42% for the nominee, via a crowdfunding operation: EUR 1.21M via Crowdcube and total capital increase of EUR 1.44M.
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:
- By broadening its clientele, while controlling the quality of the client relationship.
- By offering new products and services in order to ensure loyalty and profitability of its clientele.
- By gaining regulatory, operational, and technological autonomy in order to implement its product roadmap without the constraints related to the License / information system of its Partner PPS EU.
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:
- Renewal and internalization of the “accounts/cards” offering for French clientele, meaning Natural Persons (individuals and micro-enterprises), through a gradual migration of the Client portfolio to FR IBANs and payment cards issued by DOMINO SAS itself as a Payment Institution.
- Maintaining a partnership with PPS EU SA for Belgian clientele holding BE IBANs, meaning Natural Persons only, under the status of Agent of a Payment Service Provider, identical to the existing setup, until DOMINO SAS is able to offer its own BE IBANs through the establishment of a regulated branch in Belgium under the Freedom of Establishment (Libre Établissement). This is not yet current in the roadmap and is therefore not part of this application.
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:
- A strong and committed Content Marketing strategy on social networks (LinkedIn, TikTok and Instagram) and its website, positioning Green-Got as the reference brand on topics related to sustainable finance for the general public and reaching a broad and qualified audience. It also allows DOMINO SAS to reach several million people, without paying, on its largest Marketing operations, in particular through:
- Promotion of DOMINO SAS’s actions by its own accounts and those of its managers, with more than 420k cumulative followers on social networks.
- Encouraging its community and followers to reshare User Generated Content to promote Green-Got’s actions and Marketing operations. Some of the largest operations are reshared by several hundred people.
- Creation of sponsored content, advertising, on Instagram and TikTok social networks, enabling DOMINO SAS to reach broader targets.
- Encouraging word of mouth, in particular through a sponsorship program offering one free subscription month to both sponsor and sponsored user.
- Use of Influence Marketing, through content creators recognized for their seriousness and audience who promote DOMINO SAS.
- Strengthening the SEO (Search Engine Optimization) strategy to rank at the top of search engine results, in particular Google, for keywords related to green / impact finance. DOMINO SAS focuses mainly on “long-tail” keywords with lower search volumes but lower competition and deeper search intent, and therefore stronger purchase intent. Example: DOMINO SAS will focus on the query “How to invest responsibly in life insurance?” instead of “Green finance”.
- Sponsoring events related to sustainable development and responsible entrepreneurship to strengthen DOMINO SAS’s brand image.
- Participation in conferences, podcasts, and programs related to sustainable development, finance, and entrepreneurship.
- Maintaining strong relationships and publication of articles through various press and media organizations specialized in climate topics and general audiences, enabling it to reach a broad and qualified audience.
As part of its Payment Institution license, DOMINO SAS wishes to offer its range of products and services to:
- Legal Entity Clients (Personnes Morales), small and medium-sized enterprises, mainly VSEs and small SMEs with fewer than 50 employees in France.
- Natural Person and Legal Entity Clients resident in other Member States of the European Union / European Economic Area, except France and Belgium; see section 5.8.1, through the European Passport and the Freedom to Provide Services (Libre Prestation de Services). These Clients will be offered a FR IBAN.
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:
- Completion of the Payment Services offering:
- Issuance and acquisition of instant transfers (SCT Inst.) (2025).
- Issuance of virtual payment cards (2025).
- Proposal of new products and services:
- Accounting support tools for micro-enterprise Clients (2024) and Legal Entity Clients (2025).
- Bank card insurance (2024).
- Savings passbooks (Q4 2024) through a partnership with CFCAL, as an Intermediary in Banking Transactions and Payment Services (Intermédiaire en Opération de Banque et en Services de Paiement, IOBSP).
- Account access from a computer and web interface.
DOMINO SAS has set up waiting lists open to anyone, already a client or not, which enables it to:
- Gauge the level of expectation for its future features in order to prioritize the roadmap, with some features exceeding 10,000 people registered on the waiting lists, more than half of whom are not yet clients.
- Keep people registered on the waiting lists informed of progress on these features and their launch date.
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:
- Increased use of partnerships with companies, associations, or organizations offering services or goods complementary to the Green-Got offer, sharing its values, addressing a similar client target, and able to promote Green-Got to their customer base.
- Strengthening the Search Engine Optimization (SEO) strategy to occupy the top positions on search engines for keywords related to sustainable finance.
- Use of offline advertising, in particular posters in public transport.
- Sponsoring events related to sustainable development.
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:
- Customer service (level 1).
- First-level controls relating to onboarding, including identification, verification of Client identity, and collection of information on the purpose and intended nature of the business relationship.
- PEP / asset freeze screening.
- Transaction monitoring.
- Closure and suspicious transaction reports.
- Regulatory reporting.
In this context, DOMINO SAS:
- Equipped itself with the necessary tools, for example Ubble, now Check-Out, for identity verification; HAWK:AI for transaction monitoring; and Efficiale for Sanction / PEP / asset freeze screening.
- Developed certain tools internally: the Back Office, which makes it possible to monitor the activity and business relationship with the client.
- Sized its teams appropriately. For example, at the end of 2023, DOMINO SAS had 28 FTEs, including 4 FTEs and 2 apprentices in the Compliance Department, and 4 FTEs and 1 apprentice in Customer Service.
- Formalized a number of internal processes, for example KYC Procedure, Transaction Monitoring Procedure, Internal Control Procedure, etc.
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 map | Contracts / counterparties |
|---|---|
| Client contracts under the Agent setup | Green-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 setup | Green-Got / French end user relationships, including Green-Got general terms and the Payment Institution payment account framework contract. |
| Provider contracts | Green-Got contracts with the SEPA lead bank / safeguarding bank, Mastercard, PSEEs, and other providers. |
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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).
-
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 service | Selected | Notes | |
|---|---|---|---|
| 1° | Services enabling cash to be placed on a payment account and operations required for operating a payment account | ☐ | |
| 2° | Services enabling cash withdrawals from a payment account and operations required for operating a payment account | ☒ | |
| 3° | 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 | ☒ | ||
| 4° | 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 ☐ | |||
| 5° | 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 ☒ | |||
| 6° | Money remittance services | ☐ | |
| 7° | Payment initiation services | ☐ | |
| 8° | 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:
- Its Website: https://green-got.com/
- Its mobile application.
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.
- 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.
- Registration:
- The User fills in personal information, such as last name, first name, date of birth, etc.
- 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.
- Creation of the Personal Code:
- The User chooses a Personal Code to access the Application, with an option for biometric security.
- Choice of Card:
- The User selects the type of Card to which the User wishes to subscribe.
- 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.
- Address validation:
- The User confirms and validates the delivery address.
- Green-Got offer:
- The price and benefits associated with the Green-Got offer are presented to the User.
- Preamble: Identity Validation:
- The User is introduced to the identity verification step.
- Checklist of documents required for identity verification:
- The User is informed of the documents required to verify and validate identity.
- Verification and validation of the User’s identity via Ubble:
- The User is led to use the third-party Ubble tool, opened in the User’s browser, to verify and validate identity.
- Top-up:
- The User must deposit a fixed amount to activate the Account, via payment by bank card. To date, this is the only possible payment method, but this point is likely to evolve, in particular to allow payments by transfer.
- GDPR checks:
- The User has the option to consult detailed information relating to the General Data Protection Regulation (GDPR).
- Additional information on the User:
- The User completes the profile with additional information concerning the User, such as salary, profession, political exposure, etc.
- Communication choices:
- The User selects the types of communications the User wishes to receive, such as notifications, emails, transactional communications, promotional communications, etc.
- Finalization and congratulations screen:
- Once all steps are completed, a finalization screen is displayed, congratulating the User on opening the Account.
- Validation of the User by the DOMINO SAS Compliance Function:
- In the back office, the Compliance Function has access to all information and documents relating to the User’s identity and onboarding journey.
- Based on this information and DOMINO SAS’s internal rules, the User is validated as a Client and the Account is effectively created.
- Transmission of data to FICOBA:
- In accordance with regulations, DOMINO SAS transmits the necessary information to FICOBA (Fichier des Comptes Bancaires et assimilés), including Payment Account details and Client identification information.
- This step ensures DOMINO SAS’s compliance with applicable regulations and reinforces the security and transparency of financial transactions.
- Transmission of data to the Card manufacturer / personalizer (Exceet), see the section “Issuance of a payment card” for more details:
- Production / personalization of the Card.
- Sending of the Card to the Client.
- Address validation:
- Upon receipt of the letter containing the payment Card, the Client is invited to enter the last 4 digits of the PAN in the Application.
- This step activates the payment Card.
- This step validates the address provided by the Client.
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:
- The Card is lost or stolen: they revoke it and order a new one via the “card” and then “oppose” tabs in the Application, as shown in the screens below.
- They want a secondary physical or virtual Card.
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:
- Card screen: the User clicks “Oppose”.
- 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:
- PAN (Primary Account Number) generated from the numbers available in the IIN (Issuer Identification Number), former BIN (Bank Identification Number), that the selected card network (Mastercard) has assigned to DOMINO SAS.
- Partial PAN with only the last four digits.
- PIN (Personal Identification Number), either the same as the previous Card if there is one, or a number chosen by the User when the Card is created.
- 3-digit CVC (Card Verification Code).
- Expiration date.
- Client name as printed on the Card.
- Unique Card identifier.
- Card token to refer to the Card.
- Card personalization, such as color, design, etc.
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:
- Embossing of Client details.
- Encoding of the magnetic stripe.
- Writing to chip.
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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Arkéa processing:
- Arkéa receives the transfer on DOMINO SAS’s settlement account.
- Arkéa informs DOMINO SAS of receipt of the funds.
- 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.
- Safeguarding:
- Arkéa transfers the funds from the settlement account to the safeguarding account, on the basis of DOMINO SAS’s instructions.
- 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.
- 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.
- Authorization at the ATM:
- The ATM, operated by the bank operating the ATM, connects to the Mastercard network for transaction authorization.
- Mastercard authorization:
- Mastercard validates the Card details and forwards the authorization request to DOMINO SAS.
- DOMINO SAS authorization:
- DOMINO SAS approves the withdrawal after checking the Client’s Account balance, withdrawal limits, and all internal fraud and risk rules.
- Mastercard clearing:
- At the end of the batch period, Mastercard sends the clearing files.
- DOMINO SAS clearing:
- DOMINO SAS reviews the clearing files and performs reconciliation.
- 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.
- DOMINO SAS settlement:
- DOMINO SAS reviews the settlement files and sends the order to Arkéa to transfer the funds to the settlement account.
- Arkéa:
- Arkéa transfers funds from the safeguarding account to the settlement account.
- Arkéa transfers funds from the settlement account to Mastercard.
- Bank operating the ATM:
- Receipt of funds.
- Client:
- The Client receives confirmation of the successful or failed withdrawal via the Application or an email.
- Post-transaction monitoring by HawkAI:
- After finalization of the transaction, Hawk:AI analyzes the transactions to detect any potentially fraudulent activity. This adds an additional security layer to the process.
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.
- 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.
- The Merchant transmits the authorization request to Mastercard.
- 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.
- 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.
- Synchronous blocking sequence of checks:
- Mastercard:
- The authorization is transmitted to the Merchant.
- Merchant:
- The Merchant sends the authorized transactions to its Bank in a batch.
- Merchant’s Bank:
- The Bank initiates clearing to request Mastercard to execute the transactions.
- Mastercard:
- Mastercard transfers the clearing to DOMINO SAS for validation.
- DOMINO SAS:
- DOMINO SAS validates the clearing and returns the result to Mastercard.
- Mastercard:
- Mastercard sends the settlement files to DOMINO SAS.
- DOMINO SAS:
- Ledger update.
- Balance update.
- Two-step synchronous settlement process:
- Sending the release from safeguarding/ring-fencing (décantonnement) order to Arkéa toward DOMINO SAS’s settlement account.
- Sending the transfer order from DOMINO SAS’s card payment settlement account to Mastercard’s account.
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:
- Home: the Client accesses account actions.
- Account actions: the Client chooses “Manage my Beneficiaries”.
- 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.
- Editing an existing Beneficiary: the Client edits the Beneficiary’s last name / first name.
- Deleting an existing Beneficiary: the Client deletes a Beneficiary.
- Adding a Beneficiary: the Client adds a Beneficiary.
- Adding a Beneficiary - summary: the User accesses the summary.
- Adding a Beneficiary - code verification: the User must enter the User’s code to validate the addition.
- 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).
- 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.
- 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.
- Synchronous blocking sequence of checks:
- Arkéa:
- Desafeguarding of funds to Green-Got’s settlement account.
- Sending clearing and settlement files to the CSM.
- Sending funds to the CSM.
- CSM (EBA STEP2):
- Clearing.
- Sending funds to the Beneficiary’s Bank.
- Beneficiary’s Bank:
- Receipt of funds and notification of the Beneficiary.
- Arkéa:
- Sending the PSR (payment status report) to DOMINO SAS.
- 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.
- 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.
- 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.
- Synchronous blocking sequence of checks:
- Arkéa:
- Desafeguarding of funds to DOMINO SAS’s settlement account.
- Sending the transfer order to the CSM.
- Sending funds to the CSM.
- CSM (TIPS):
- Sending funds to the Beneficiary’s Bank.
- Beneficiary’s Bank:
- Receipt of funds and notification of the Beneficiary.
- Arkéa:
- Sending the PSR (payment status report) to DOMINO SAS.
- 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).
- 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.
- 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.
- 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.
- 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.
- Synchronous blocking sequence of checks:
- 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.
- CSM (EBA STEP2):
- Clearing.
- Sending funds to the creditor’s bank.
- Creditor’s bank:
- Receipt of funds and notification of the creditor.
- Arkéa:
- Sending the PSR (payment status report) to DOMINO SAS.
- 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:
- Execution of an agreement between DOMINO SAS and the beneficiary of the payment transactions (the Client):
- DOMINO SAS, as PSP, enters into an agreement with the beneficiary Client, Natural Person or Legal Person, to receive payments on the Client’s behalf. This agreement forms an integral part of the general terms and conditions, which are accepted by the Client when the Account is opened.
- The agreement specifies payment processing methods, fund availability timelines, and service fees.
- Receipt of payment orders:
- DOMINO SAS receives payment transactions initiated by third-party payers in other banks via Arkéa transmissions. These third parties may be:
- Individual or professional clients of the beneficiary that has an Account with DOMINO SAS, in the case of a business account.
- Individuals who have the DOMINO SAS client who must receive the funds among their beneficiaries and who have made a transfer to that client.
- Companies, or any other entity, that must send money to a DOMINO SAS client, for example a company paying a salary or a CAF paying an allowance.
- Each transaction is processed in accordance with the terms of the agreement.
- DOMINO SAS receives payment transactions initiated by third-party payers in other banks via Arkéa transmissions. These third parties may be:
- Funds received on DOMINO SAS’s settlement account at Arkéa are protected in the safeguarding account and the balance of the beneficiary Client’s Account is updated.
- The beneficiary Client is informed instantly of receipt of the funds and transaction details.
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.
- 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.
- 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.
- Personal Information:
- In personal information, the User identifies the option to initiate the Account closure procedure.
- 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.
- 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.
- 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.
- 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:
- Trigger: confirmed fraud, money laundering, or terrorist financing.
- Initialization of the closure process, where applicable with an email requesting bank details (RIB) in the Client’s name.
- Preparation of a Suspicious Activity Report (SAR).
- Collection of KYC information and extraction of transaction history.
- Sending of the SAR dossier.
- Recording the date on which the SAR was sent and retaining the Client’s data for 10 years.
In addition:
- A period of no more than 2 days elapses between the result of the alert analysis and the drafting of the suspicious transaction report (SAR in the diagram for Suspicious Activity Report).
- DOMINO SAS retains the Report on its drive network and the KYC and transaction information of the closed Client for a period of 10 years.
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 […]“)
| Yes | ☒ | No | ☐ |
|---|
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:
- Products and Services offered as CIF: a Life Insurance Product, in partnership with Generali, through which the Client may, through the DOMINO SAS Application, subscribe to a life insurance contract. DOMINO SAS analyzed all Unit-Linked funds (equity and/or bond) available in Generali’s fund universe and selected the 25 that best met its financial and extra-financial criteria, including exclusion of companies linked to fossil fuels, environmental preservation, and CO2 emissions reduction, using third-party data such as Carbone 4, Morningstar, Ugerwald, and environmental labels.
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.
- Products and Services offered as IOBSP: non-regulated savings passbooks, in partnership with CFCAL, accessible directly from the Application. The amounts collected through these passbooks will be allocated to the granting of loans recorded on CFCAL’s balance sheet, intended in particular to finance low-carbon mobility solutions and thermal renovation of buildings. Opening a passbook and all related operations are accessible from the Application.
Ancillary Services
(Article L.522-2, I of the French Monetary and Financial Code)
| Ancillary service | Selected |
|---|---|
| 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.
| Yes | ☒ | No | ☐ |
|---|
DOMINO SAS plans to carry out its activities under Freedom to Provide Services in all the following EEA countries:
- Germany.
- Austria.
- Belgium.
- Bulgaria.
- Cyprus.
- Croatia.
- Denmark.
- Spain.
- Estonia.
- Finland.
- Greece.
- Hungary.
- Ireland.
- Iceland.
- Italy.
- Latvia.
- Liechtenstein.
- Lithuania.
- Luxembourg.
- Malta.
- Norway.
- Netherlands.
- Poland.
- Portugal.
- Czech Republic.
- Romania.
- Slovakia.
- Slovenia.
- Sweden.
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.
| Yes | ☐ | No | ☒ |
|---|
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 concerns | Total |
|---|---|
| Purchasing power | 40 |
| Personal health and the health of close relatives | 37 |
| The environment, including climate change, biodiversity and pollution | 32 |
| Incivility and crime | 32 |
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:
-
Global awareness among private economic actors and individuals of the importance of climate issues;
-
The strengthening of applicable regulations, in particular in the European Union, the sector’s leading market in 2022 with nearly 39% market share.
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:
-
Revolut: 25.5 million customers in the EU and United Kingdom9;
-
Wise: 16 million;
-
N26: 8 million;
-
Monzo: 7 million;
-
Lydia: 5.5 million.
Professional customers are an essential growth driver for these actors in terms of number of customers and profitability:
-
Revolut for Business: 500K customers in Europe;
-
Qonto: 400K;
-
Shine: 100K;
-
Pleo: 30K.
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:
-
Fundraising: EUR 1.7 billion raised by Revolut, EUR 900 million for N26, EUR 500 million by Monzo10 and Qonto;
-
Exits: acquisitions of Shine or Nickel by Société Générale and BNP Paribas for several tens or even hundreds of millions of euros.
“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:
-
Triodos Bank: present in Germany, Spain, the Netherlands and the United Kingdom, this actor already has more than 740,000 customers (launched in 1980);
-
Tandem Bank: present in the United Kingdom, this actor already has more than 280,000 customers (launched in 2014);
-
Tomorrow: present in Germany, this actor already has more than 120,000 customers (launched in 2017);
-
Bunq: one of the main actors present in the Netherlands, with 9 million customers, has also launched a “sustainable” offering and is fully engaged in the approach of offering “sustainable” financial services.
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:
-
Crédit Coopératif: 100,000 customers to date (launched in 1893);
-
La Nef: 80,000 customers to date (launched in 1984).
Positioning of DOMINO SAS
DOMINO SAS and its Green-Got offering position themselves in relation to several categories of actors:
-
Traditional actors, mainly Credit Institutions, which can be divided into two categories:
-
Generalist actors such as Société Générale, BNP Paribas or Crédit Agricole;
-
Actors specialized in sustainable development, such as La Nef or Crédit Coopératif.
-
-
Fintechs, including:
-
Providers of payment accounts, electronic money wallets and other generalist financial services such as N26, Revolut or Lydia;
-
Payment and investment actors specialized in impact, such as Tomorrow or Goodvest, which are Green-Got’s most direct competitors.
-
Strengths and weaknesses of traditional banking actors in the sustainable financial services market:
-
Strengths:
-
Traditional actors have substantial financial power, enabling them to offer an unrivaled product range at a reasonable price, at least for their online subsidiaries, and very significant marketing budgets that allow them to maintain a very strong presence across all major traditional distribution channels;
-
They are already established, have millions of customers, thousands of branches and significant longevity evidencing their financial soundness. They have the trust of their customers, especially during periods of financial crisis, and in particular French Credit Institutions, whose reputation is solid.
-
-
Weaknesses:
-
These actors have a very poor reputation on environmental issues, being frequently accused by various NGOs because of their massive support for sectors such as fossil fuels, including oil, coal and gas, and the financing of their expansion, as evidenced by the Banking on Climate Chaos report13. This should not be generalized, however: certain actors, such as Crédit-Mutuel Arkéa or La Banque Postale, generally rank much better than their direct competitors;
-
They have limited command of contemporary marketing codes and have difficulty adapting to more modern distribution channels, especially social networks;
-
Some have particularly high fee schedules;
-
They may suffer from significant technological delay, preventing them from launching certain functionalities quickly and agilely;
-
They address very broad personas with very varied financial needs, which condemns them to a certain inertia from a technological and marketing point of view.
-
Strengths and weaknesses of actors specialized in sustainable development in the sustainable financial services market:
-
Strengths:
-
They have a better environmental brand image than traditional actors, in particular from the point of view of NGOs;
-
They are already established, have tens of thousands of customers and strong local anchoring;
-
They are perceived as more financially robust than Fintechs, especially because of their close links with major banking groups.
-
-
Weaknesses:
-
They suffer from a very significant technological delay compared with Fintechs and lack functionalities. La Nef, for example, is not able to offer a payment account (compte de paiement), although that product is the basis of any financial services offering;
-
They have very limited command of modern marketing codes and have difficulty adapting to more modern distribution channels, especially social networks;
-
They are often perceived as “old and outdated”, especially by younger people, having failed to capture a large market share despite several decades of existence.
-
Strengths and weaknesses of generalist Fintechs in the sustainable financial services market:
-
Strengths:
-
They have the best technologies, the most modern functionalities and great agility, enabling rapid progress;
-
Some have already raised hundreds of millions of euros, enabling them to recruit many employees and acquire many customers, sometimes at a high cost;
-
They are very inexpensive for customers, and sometimes free.
-
-
Weaknesses:
-
Their business model remains fragile, with the majority still not profitable and having difficulty monetizing Customers attracted by high referral bonuses and free offers;
-
They are very frequently used as secondary accounts and have difficulty retaining their Customers over time;
-
Although they do not suffer from a particularly negative image in environmental areas, their voice on these topics remains inaudible even though consumers now expect brands to engage more on major societal issues;
-
Few are already Credit Institutions, which limits the range of products and services they can offer directly.
-
Strengths and weaknesses of Impact Fintechs in the sustainable financial services market:
-
Strengths:
-
They are driven by a strong mission and appreciated for their commitment, reducing their acquisition costs;
-
They focus on a very specific category of the population, which makes it possible to deliver a clear marketing message and better prioritize the technology roadmap and the feature roadmap.
-
-
Weaknesses:
-
They are very recent and must still prove their financial soundness and reassure potential Customers;
-
They focus on specific financial services, whether payments or investment, but do not yet offer a complete product range to their users.
-
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:
-
A strong mission and a real ambition to act for climate protection, shared by its Customers;
-
Responsive and attentive Customer Service, reflected in a 4.9/5 rating on the application download portals;
-
An attractive price for Life Insurance, with no entry, payment, surrender or arbitration fees and management fees of only 1%, and therefore very attractive, below the average fees charged by traditional actors;
-
An innovative product using modern technologies that can adapt quickly to customer requirements;
-
A narrower and younger target that allows DOMINO SAS to focus its efforts and Marketing messages on limited channels with the same message each time. This more concentrated target also allows for a clearer roadmap by avoiding dispersion effects linked to a customer portfolio that is too diverse in expectations and needs;
-
A team that is close to and accessible to its customers: DOMINO SAS regularly organizes meetings and webinars to meet its Customers, answer their questions and listen to their suggestions. In-person meetings are organized every 2 to 3 months and each time bring together more than one hundred people.
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:
-
Innovative payment functionalities:
-
Apple Pay and Google Pay mobile payment;
-
Unlimited-access piggy banks to facilitate budget management;
-
Instant notifications for each payment, including card payments, outgoing transfers and direct debits;
-
Simple management of payment limits and card limits;
-
Unlimited fee-free card payments worldwide.
-
-
Innovative impact functionalities:
-
The “Impact” page to track in real time the positive impacts financed by the Green-Got community;
-
The “CO2 Calculator” to estimate the CO2 impact of each expense;
-
Donations made to associations working to protect the planet: at each payment through round-up and donations based on interchange fees;
-
One of the lowest carbon intensities on the market according to Magelan.
-
-
Impact savings functionalities:
-
Life insurance to invest in companies specialized in the energy and climate transition, with the launch of GG Planet scheduled for Q1 2024, in partnership with Generali and structured around the following characteristics:
-
Four portfolios composed of securities and bonds adapted to each DOMINO SAS customer according to their profile;
-
An in-depth methodology for selecting investment products14, the strictest fossil-fuel exclusions on the market through partnerships with Carbone 4 and Ugerwald, and a focus on low-carbon transport and energy, responsible agriculture, better natural-resource management, etc.;
-
Real-time and fully transparent consultation of the impact of the subscribed life-insurance product: avoided or reduced emissions, climate trajectory and carbon impact of the associated investments;
-
100% mobile access, which remains rare for an investment offering.
-
-
Non-regulated passbooks (livrets non réglementés), with launch scheduled for Q4 2024, in partnership with CFCAL, for precautionary savings enabling financing of the ecological transition, including sustainable mobility, thermal renovation and sustainable real estate. DOMINO SAS is not involved in the choice and associated financing terms.
-
-
A 360-degree approach: unlike most Impact Fintechs, DOMINO SAS aims to offer its customers both payment services and savings services to make daily life even easier for Customers by centralizing all their financial needs in a single application;
-
The strength of its community: a “pool” of more than 2,000 ambassadors and an active, engaged community on social networks facilitate DOMINO SAS’s customer acquisition through word-of-mouth and User Generated Content. No remuneration of any kind is associated with these initiatives. For each DOMINO SAS marketing operation, several hundred of these ambassadors repost and share publications around the Green-Got brand on social networks, notably LinkedIn and Instagram. Again, no remuneration is provided in return. It is not rare for some communications to exceed one million impressions with no financial impact for DOMINO SAS. The ambassadors also make it possible to collect many user feedback items rapidly, thereby facilitating constant improvement of the application and associated services;
-
Responsive Customer Service, reachable by telephone in addition to email, chat or social networks, thereby enabling even greater proximity with DOMINO SAS Customers;
-
Pre-accounting services, including invoice downloads and
.xlstransaction exports, and invoicing services, including quotes and invoices, will be available for “Sole Traders” and Business Customers from the launch of the B2B offering in Q1 2025.
Benchmark of offerings and features - Individuals (actors specialized in sustainable development / Impact Fintechs):
| Criterion | Green-Got | Crédit Coopératif | La Nef | Bunq | Triodos Bank | Tandem Bank | Tomorrow | Helios | Goodvest |
|---|---|---|---|---|---|---|---|---|---|
| Countries present | France, Belgium | France | France | Netherlands, Germany, France, Italy & Spain | Germany, Spain, United Kingdom & Netherlands | United Kingdom | Germany | France | France |
| Number of customers | 19,500 | 100,000 | 80,000 | 9m | 740,000 | 280,000 | 120,000 | 17,500 | 5,000 |
| Launch date | June 2022 | 1893 | 1986 | 2012 | 1980 | 20212 (as shown in source visual) | 2017 | 2021 | 2021 |
| Payment account / current account | Yes | Yes | No, business only | Yes | Yes | No | Yes | Yes | No |
| Preferred segment | Individuals aged 25-40 | Association | Enterprises | Individuals aged 25-40 | Individuals of all ages | Individuals of all ages | Individuals of all ages | Individuals of all ages | Individuals aged 25-40 |
| Customer type | Individual; Enterprise (Q1 2025); Micro-Enterprise | Association; Individual; Enterprise; Micro-Enterprise | Enterprise; Individual; Enterprise; Micro-Enterprise | Individual; Enterprise; Micro-Enterprise | Individual; Enterprise; Micro-Enterprise | Individual | Individual | Individual | Individual |
| Monthly fee for payment services | EUR 6 incl. VAT (TTC) | EUR 3.7 incl. VAT per month | N/a | EUR 17.99 incl. VAT | From EUR 5.5 | N/a | EUR 7 then EUR 8 in March 2023 incl. VAT | EUR 6 incl. VAT | N/a |
| French IBAN | Yes | Yes | Yes | Yes | No | No | No | No | N/a |
| SEPA transfer | Free and unlimited | Free and unlimited | Free and unlimited | Free and unlimited | Free and unlimited | N/a | 30 per month | 30 per month | N/a |
| Round-up to finance impact projects | Yes | Yes | No | No | No | N/a | No | No | N/a |
| Customer Service | 6/7 telephone & email | 6/7 telephone & email | 6/7 telephone & email | 6/7 by email | 6/7 by email | 6/7 by email | 5/7 by email | 5/7 by email | 5/7 by email |
| Foreign-currency payment | Free and unlimited | 2.9% of amount | N/a | Free and unlimited | Free and unlimited | N/a | 1% of amount | 1% of amount | N/a |
| Customer ratings | 4.9/5 | 1.8/5 | 4.3/5 | Not shown | 3.9/5 | 4.3/5 | 4.7/5 | 4.3/5 | 4.9/5 |
| CO2 footprint calculation for expenses | Yes | No | No | No | No | N/a | Yes | No | N/a |
| Apple & Google Pay | Yes | Yes | N/a | Yes | Yes | N/a | Yes | No | N/a |
| Piggy bank & vault | Yes | No | N/a | Yes | No | N/a | Yes | Yes | N/a |
| Donation to NGOs at each payment | Yes | Yes | No | Yes | No | N/a | Yes | No | N/a |
| Carbon intensity of deposits | 145 t/mEUR | 121 t/mEUR | 121 t/mEUR | Unknown | Unknown | N/a | Unknown | 148 t/mEUR | N/a |
| Savings & investment product | Life insurance (Q1 2024); passbooks (Q4 2024) | Life insurance; passbooks | Passbooks | Equivalent passbooks | Equivalent passbooks & securities account | Equivalent passbooks & securities account | Equivalent passbooks & securities account | No | Life insurance |
Benchmark of offerings and features - Sole Traders (generalist Fintechs):
| Criterion | Green-Got | Qonto | Shine | Blank |
|---|---|---|---|---|
| Countries present | France | France, Spain, Italy & Germany | France | France |
| Number of customers | 1,000 | 400,000 | 150,000 | 20,000 |
| Launch date | April 2023 | 2016 | 2017 | 2021 |
| Preferred segment | Micro-Enterprise | SME (PME) | Micro-Enterprise | Micro-Enterprise |
| Monthly fee for payment services | EUR 5 excl. VAT (HT) | EUR 9 excl. VAT | EUR 7.9 excl. VAT | EUR 6 excl. VAT |
| SEPA transfer | Free and unlimited | 30 per month | 30 per month | 30 per month |
| Round-up to finance impact projects | Yes | Yes | No | No |
| Customer Service | 6/7 telephone & email | 7/7 chat & email | 7/7 telephone & email | 7/7 chat & email |
| Foreign-currency payment | Free and unlimited | EUR 1 + 1.9% of amount | 2% of amount | 1.9% of amount |
| Customer ratings | 4.9/5 | 4.7/5 | 4.7/5 | 4.3/5 |
| CO2 footprint calculation for expenses | Yes | No | No | No |
| Apple & Google Pay | Yes | Yes | Yes | Yes |
| Piggy bank & vault | Yes | No | N/a | No |
| Donation to NGOs at each payment | Yes | No | No | No |
| Pre-accounting tools | Q1 2025 | Yes | No, not on this offer | Yes |
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:
-
Regulated payment services:
-
Produced and distributed under the future Payment Institution (Établissement de Paiement) authorization;
-
Produced and distributed under the status of Agent of PPS EU SA.
-
-
Regulated services distributed under the CIF status for financial savings and the IOBSP status for bank savings.
DOMINO SAS’s revenue model will therefore rely on the combination of the following revenue lines:
-
Revenue linked to payment services distributed via the Payment Institution authorization: contribution from 85% in year N1, the first year as an authorized Payment Institution, to 79% in year N5. The decrease is associated with the gradual relative increase in commissions linked to savings and investment products distributed by DOMINO SAS and produced or managed by partners:
-
Subscriptions of between EUR 5 and EUR 15 excluding VAT for Natural Persons (Personnes Physiques) and EUR 35 to EUR 50 excluding VAT for Legal Entities (Personnes Morales), contributing between 66% and 51% of revenue;
-
Interchange commissions on debit cards of around 0.2% for Natural Person transactions and between 1.5% and 2%, depending on transaction type, for Legal Entity transactions, contributing between 17% and 27% of revenue;
-
Fees applicable to customers, contributing between 2% and 1% of revenue:
-
ATM withdrawals: withdrawals made at ATMs will be invoiced to Customers beyond a certain number of monthly transactions, which varies depending on account type. Withdrawal invoicing will represent between 1.4% and 0.8% of revenue;
-
Incident fees: these fees will apply in particular when irregular use of the account creates additional work for the compliance and/or customer service teams. These fees will represent less than 0.05% of DOMINO SAS revenue;
-
Ordering a new physical card before the expiry date: any order for a new physical card before its expiry date will be invoiced to the Customer at EUR 10. These fees will contribute less than 0.1% of total revenue.
-
-
-
Revenue linked to management commissions as CIF and IOBSP, for an amount between 15% and 21% of DOMINO SAS revenue and composed of:
-
Life insurance: an offering for Natural Person customers only, structured around four portfolios with different risk levels and composed of unit-linked funds (equities and bonds) of sustainable companies. DOMINO SAS will be remunerated through management commissions, annual commissions of 1% of the total amount collected. No entry or exit fee is associated with this product. DOMINO SAS aims to reach EUR 10,700 average collected per contract after the 5th year of commercialization of this offering, corresponding to the average life-insurance outstanding amount for 25-34 year-olds15. This offering will represent between 13% and 17% of DOMINO SAS revenue;
-
Crowdfunding offering: an offering for Natural Person customers only. DOMINO SAS revenue associated with this offering will amount to 2.5% of amounts collected. DOMINO SAS aims to collect EUR 769 per Investor per year in year 4 after launch. This is a conservative figure, since the average crowdfunding investment in France is currently EUR 2,87116. This offering will represent between 0% and 2.4% of DOMINO SAS revenue;
-
Non-regulated savings passbook: an offering for all types of DOMINO SAS customers, Natural Persons and Legal Entities. DOMINO SAS will receive remuneration corresponding to 0.2% per year of total outstanding amounts. DOMINO SAS aims for an average outstanding amount of EUR 5,397 for Individuals in year N5. By comparison, the average deposit on a regulated passbook is currently around EUR 6,35117. This offering will represent between 1.6% and 1.9% of DOMINO SAS revenue.
-
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:
-
DOMINO SAS payroll, with a relative contribution from 29% in year N1 to 24% in year N5, of which the variable portion linked to the number of Customers and associated business volumes, in particular the AML/CFT (lutte contre le blanchiment de capitaux et le financement du terrorisme, LCB-FT) and customer service teams, will exceed 65% in 2029;
-
Marketing/customer acquisition expenses, from 20% to 35%, more than 90% variable.
The other expense items will be:
-
Expenses linked to DOMINO SAS’s Core Banking System and associated transactions: connection to the SEPA network, safeguarding of funds (cantonnement des fonds), Card Processing, fraud cost and other costs linked to payment operations. The share of these costs in DOMINO SAS expenses will be relatively stable, excluding year N1: they should represent between 15% in year 2 and 14% in year 5. In year N1, these costs will represent 25% of expenses because of additional non-recurring costs linked to the transition from a 100% Agent of PPS EU SAS model to a Payment Institution model;
-
Other expenses linked to payment operations: KYC/KYB (Ubble), electronic signatures (Yousign), insurance associated with payment cards and donations of part of interchange fees to associations. These expenses will represent between 13% and 15% of total projected expenses for years N1 to N5;
-
Tools and monthly recurring subscriptions: mainly IT expenses, including Cloud, servers, tools, Design, security, VPN and Web development, and Marketing expenses, including CRM, SMS/email and tracking, for a stable amount of 6% over years N1 to N5;
-
“G&A” expenses: notably linked to different partners, including accountant, periodic control, statutory auditor and legal counsel, or other operating expenses, including rent and other administrative costs, for a stable amount of 4% over years N1 to N5.
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:
-
1.4 Annual accounts of DOMINO SAS as at 31.12.2021;
-
1.5 Annual accounts of DOMINO SAS as at 31.12.2022;
-
1.6 Annual accounts of DOMINO SAS as at 30.09.2023.
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:
-
01/10/2024: start date of the Payment Institution activity;
-
Calendar-year logic, except for the first year of operation (N1) of the Payment Institution, lasting 15 months from 01/10/2024 to 31/12/2025.
Historical data are based on the following:
-
Historical accounting data for 2020-2021 (N-4) and 2022 (N-3);
-
A 2023 forecast (N-2) based on the landing expected by DOMINO SAS’s Finance Department;
-
A 2024 forecast (N-1) of 9 months, from 01/01/2024 to 30/09/2024.
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:
-
B2C Natural Persons: compound annual growth rate (CAGR) of 33% in number of accounts to reach a total of 170,000 accounts at the end of N5, including 74% “Standard” accounts and 26% “Premium” accounts;
-
B2C Sole Trader: CAGR of 43% in number of accounts to reach a total of 54,000 accounts at the end of N5, including 61% “Standard” accounts and 39% “Premium” accounts;
-
B2B: CAGR of 60% in number of accounts to reach a total of 40,000 accounts at the end of N5, including 75% “Standard” accounts and 25% “Premium” accounts.
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:
-
For B2C Natural Persons: influence and User Generated Content;
-
For B2B: strong focus on partnerships with companies offering services complementary to those of DOMINO SAS, for example LiveMentor, SuperIndep and Envi, and promoting Green-Got services. These partnerships may range from a simple visibility exchange to affiliation contracts under which these partners will be remunerated each time a customer subscribes to the Green-Got offering using the partner code;
-
For B2C Sole Trader: a hybrid Marketing approach between B2C Natural Person and B2B.
Improvement and addition of new functionalities will also strengthen customer acquisition, in particular:
-
Launch of a non-regulated passbook offering, accessible to any type of customer from N-1 for Natural Persons and N1 for Legal Entities;
-
Ability to make and receive SEPA transfers instantly, at no additional fee for any type of customer from N1;
-
Access to accounts from a website, and no longer only through the Application, from mid-N-1;
-
Launch of joint accounts (1 account = 2 cards) from N-1 for B2C Natural Person customers only;
-
Improvement of tools for calculating CO2 emissions linked to transactions.
DOMINO SAS customer projections (central scenario), end of period:
| Customer segment | N-4 2020 & 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Total customers / segment total | - | 10,077 | 21,525 | 33,348 | 67,797 | 100,713 | 145,308 | 198,651 | 264,200 |
| Individual Customer - Standard | - | 10,077 | 20,597 | 27,428 | 44,766 | 59,838 | 78,100 | 100,044 | 126,807 |
| Micro-Enterprise - Standard | - | - | 928 | 2,308 | 5,950 | 9,896 | 15,535 | 23,009 | 33,184 |
| Individual Customer - Premium | - | - | - | 2,755 | 10,203 | 16,753 | 24,387 | 33,242 | 43,749 |
| Micro-Enterprise - Premium | - | - | - | 857 | 3,079 | 5,644 | 9,399 | 14,303 | 20,890 |
| Enterprise - Standard | - | - | - | - | 2,841 | 6,398 | 13,414 | 21,013 | 29,570 |
| Enterprise - Premium | - | - | - | - | 958 | 2,186 | 4,473 | 7,040 | 10,000 |
DOMINO SAS’s economic model is based on:
-
Growth in its customer base enabled by the following actions:
-
Continuation of a proven and effective Marketing and customer acquisition strategy;
-
New functionalities, in particular:
-
Launch of joint accounts (Q1 2024);
-
Expansion of the types of Customers served with the launch of B2B offerings (Q1 2025);
-
Expansion of the services offering to other European Union countries through the Freedom to Provide Services, prioritizing Germany, Spain, Italy and the Netherlands;
-
-
-
Generation of additional revenue linked to new services, particularly:
-
Launch of savings products, Life Insurance (Q1 2024), Passbooks (Q4 2024), and investment products, Crowdfunding;
-
Incentives to upgrade, in particular additional Insurance on Premium accounts, privileged access to Customer Service, special partner offers, including early access to features, etc., supported by occasional Marketing offers enabling customers to benefit from them at a reduced price, with no remuneration for DOMINO SAS.
-
-
Strengthening of account usage rates, including number of debit or credit transactions and account balances, across the customer base, encouraged by:
-
Access to accounts from the web, with no obligation to first download the application (Q1 2024);
-
Ability to make instant transfers, an offering not currently possible under the PPS EU SA Agent model;
-
Marketing incentives to have salary paid into the account.
-
Forecast evolution of DOMINO SAS revenue (central scenario):
| Revenue line | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total revenue | EUR 353,207 | EUR 1,102,068 | EUR 1,868,339 | EUR 8,199,518 | EUR 16,112,470 | EUR 28,654,130 | EUR 46,524,416 | EUR 72,139,212 |
| Subscriptions | EUR 296,585 | EUR 933,010 | EUR 1,406,585 | EUR 5,437,698 | EUR 9,419,227 | EUR 16,143,282 | EUR 24,518,950 | EUR 36,575,064 |
| Other payment-account revenue | EUR 6,933 | EUR 18,335 | EUR 35,286 | EUR 134,288 | EUR 210,375 | EUR 330,699 | EUR 488,389 | EUR 668,225 |
| Interchange | EUR 49,690 | EUR 146,400 | EUR 291,280 | EUR 1,406,758 | EUR 3,852,988 | EUR 7,321,837 | EUR 12,562,215 | EUR 19,622,779 |
| Investment revenue | EUR - | EUR 4,323 | EUR 135,188 | EUR 1,220,774 | EUR 2,629,880 | EUR 4,858,312 | EUR 8,954,863 | EUR 15,273,144 |
| Total split | 100% | 100% | 100% | 100% | 100% | 100% | 100% | 100% |
| Subscription split | 84% | 85% | 75% | 66% | 58% | 56% | 53% | 51% |
| Other payment-account revenue split | 2% | 2% | 2% | 2% | 1% | 1% | 1% | 1% |
| Interchange split | 14% | 13% | 16% | 17% | 24% | 26% | 27% | 27% |
| Investment revenue split | 0% | 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:
-
Strong growth in its active customer base: from 68K to 264K active customers over 5 years;
-
An increase in average annual revenue per active customer from EUR 118 to EUR 273 over a 5-year horizon;
-
To achieve this, DOMINO SAS will rely on the four pillars of its revenue model:
-
Revenue from monthly account fees (subscriptions): expected decrease in the contribution of subscriptions, from 66% to 51% over 5 years, despite a volume effect from customer-count growth and a price effect from the tariff increase shown in the table below, due in particular to the growing share of interchange commissions and revenue linked to savings and investment products distributed by DOMINO SAS;
-
Revenue from interchange: gradual increase in the relative share of this revenue type to reach an average level around 27% of revenue in year N5, compared with 17% in year N1. This progression is explained by the launch of B2B cards and the growing share of associated card transactions in the customer mix, with the total euro amount of B2B transactions rising from 12% in N1 to 40% in N5, an average of EUR 68 per transaction versus EUR 40 for B2C, combined with higher interchange rates than for B2C cards;
-
Revenue from activity as payment-services agent for PPS EU SAS: very large decrease in the relative share of this revenue source until it becomes nearly zero, compared with 53% in year N1;
-
Revenue linked to management and collection fees for savings and investment: a vector for diversification and increasing revenue per customer. These elements will see their relative weight increase sharply, from 15% to 21%, by year N5.
-
The monthly subscription amount by account type will evolve between N-1 and N5 as shown below:
| Monthly subscription excl. VAT (HT), in EUR | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Individual Customer - Standard | 5.00 | 5.00 | 5.00 | 5.00 | 5.00 | 6.00 | 6.00 | 7.00 |
| Micro-Enterprise - Standard | - | 5.00 | 7.00 | 7.00 | 7.00 | 8.00 | 8.00 | 9.00 |
| Individual Customer - Premium | - | - | 10.00 | 10.00 | 12.00 | 12.00 | 12.00 | 13.00 |
| Micro-Enterprise - Premium | - | - | 12.00 | 12.00 | 14.00 | 14.00 | 14.00 | 15.00 |
| Enterprise - Standard | - | - | - | 35.00 | 35.00 | 35.00 | 35.00 | 35.00 |
| Enterprise - Premium | - | - | - | 50.00 | 50.00 | 50.00 | 50.00 | 50.00 |
Revenue linked to interchange commissions will remain the same until year N5 under the following assumptions:
-
0.2% for all types of B2C Natural Person card transaction;
-
1.5% for B2B physical card transactions in the EEA;
-
2.0% for B2B physical card transactions outside the EEA;
-
1.9% for B2B 3DS transactions.
Revenue linked to activities will remain the same until year N5 with:
-
1% per year of total sums invested, on total outstanding amounts, in Life Insurance;
-
0.2% per year of total sums invested, on total outstanding amounts, in passbooks;
-
2.5% per year of total newly collected amounts in Crowdfunding.
DOMINO SAS’s expense model:
The main expense assumptions of DOMINO SAS are:
-
Payroll, including remuneration of the Effective Managers (Dirigeants Effectifs), other members of the Management Committee and operational teams, will represent 24% of total expenses over a 5-year horizon, compared with 30% in year N1;
-
Expenses directly linked to activity (Cost of Goods Sold) will reach a level close to 35% of operating expenses in year N5, broken down around the following main expense types:
-
Core Banking System costs, including card and SEPA transaction Processing and costs linked to Mastercard: the relative share of these expenses should stabilize from year N2 and remain close to 12% of operating expenses for years N2 to N5. The relative weight of the Core Banking System will be particularly high in year N1 because of costs linked to migration from PPS EU SA tools to DOMINO SAS’s proprietary Core Banking System. These costs will be broken down as follows in year N5:
-
43% for costs linked to Mastercard;
-
24% for costs linked to cash withdrawals at ATMs;
-
15% for costs linked to 3DS transactions;
-
15% for costs linked to SEPA transfers, In and Out, including 89% for instant transfers;
-
Costs of donations of interchange fees to NGOs: 10% of Operating Expenses in year N5, compared with 7% in year N1.
-
-
Other costs linked to Life Insurance for 2% of Operating Expenses, including costs linked to electronic signatures and contract archiving;
-
Costs linked to DOMINO SAS taking out insurance covering cardholders for 2% of Operating Expenses in year N5, with negotiations in progress with several insurance companies and the estimate based on pricing elements shared to date;
-
Card manufacturing, personalization and shipping costs invoiced by Exceet for 1.3% of total operating expenses in year N5, compared with 5.2% in year N1 because of replacement of cards for Customers migrating from the PPS EU SA Agent model to the Payment Institution model and its own BIN, and 2.7% in year N2. This amount will decrease over the years for several reasons:
-
Limited increase in card prices (+1.0% per year), because lower unit costs linked to higher volumes only partially offset inflation, with Exceet’s pricing grid decreasing based on card volume;
-
Relative decrease in the weight of wooden cards, which are reserved for B2C customers and more expensive than plastic cards;
-
5% annual growth in the share of customers choosing an offering without a physical card, linked to the progress of mobile and online payment;
-
-
Miscellaneous expenses, including provisions for transaction losses and fraud, for 2% of Operating Expenses in year N5;
-
Onboarding expenses linked to analysis of customer subscription requests, including KYC/KYB and top-up, for 1% of Operating Expenses in year N5.
-
-
Marketing and communication expenses, representing 27% of expenses on average over the 5 years of the Business Plan;
-
IT expenses, including software and other technological equipment, for 6% of Operating Expenses over the 5 years of the Business Plan, broken down as follows:
-
48% for development costs themselves as well as the cost of tools linked to development and maintenance of the technological infrastructure:
-
Core Banking System, processing building blocks, etc.;
-
Hosting tools (AWS) and database tools (MongoDB, Datadog);
-
VPN (Tailscale);
-
Tools linked to IT development (Vercel, Expo, Github).
-
-
Nearly 50% for other IT expenses borne by DOMINO SAS, mainly Marketing tools (17%), administrative tools (15%) and Customer Service tools (8%).
-
-
“G&A” expenses:
-
Expenses linked to team management, including office rental, team-building events and others, for 3% of operating expenses in year N5;
-
Advisory fees, including legal, regulatory and recruitment, and other service provisions, including statutory audit, periodic control, various insurance and mission/travel expenses, for 1% of operating expenses in year N5.
-
DOMINO SAS staffing:
-
DOMINO SAS’s development plan requires appropriate growth in staffing and in particular:
-
A Customer Support team responsible for assisting customers. DOMINO SAS has chosen strong proximity with its customers, and the responsiveness of this team and the quality of answers provided constitute a very strong differentiating axis compared with competitors, whether traditional actors or Fintechs. This strategy therefore translates into the creation of a substantial internal team without recourse to outsourcing. This team will represent 32% of staffing at the end of year N5.
-
A compliance team responsible for KYC/KYB, Transaction Monitoring and other AML/CFT operations. DOMINO SAS has chosen to allocate significant resources to this team, which will represent 25% of staffing at the end of N5.
-
An IT & Products team responsible for developing new functionalities, improving IT services and maintenance. DOMINO SAS has chosen strong internalization of these capabilities to retain autonomy and independence in product roadmap matters. This team will therefore represent 20% of staffing at the end of year N5.
-
Forecast evolution of DOMINO SAS staffing (central scenario), end of period in number of FTEs:
| FTE category | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total FTEs, end of period | 16 | 27 | 35 | 56 | 84 | 113 | 143 | 173 |
| Admin & support functions | 1 | 3 | 3 | 6 | 9 | 13 | 15 | 16 |
| IT & Product | 7 | 10 | 12 | 16 | 22 | 26 | 31 | 34 |
| Marketing | 4 | 5 | 6 | 8 | 9 | 11 | 13 | 16 |
| Customer Support | 2 | 5 | 5 | 11 | 21 | 34 | 42 | 56 |
| Compliance | 2 | 4 | 6 | 12 | 19 | 24 | 36 | 43 |
| Internal Control & Risks | - | - | 3 | 3 | 4 | 5 | 6 | 8 |
Forecast evolution of DOMINO SAS staffing (central scenario), annual average in number of FTEs:
| FTE category | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total FTEs, annual average | 11 | 20 | 28 | 41 | 70 | 95 | 129 | 158 |
| Admin & support functions | 1 | 2 | 3 | 4 | 8 | 11 | 15 | 16 |
| IT & Product | 4 | 8 | 11 | 13 | 19 | 24 | 28 | 33 |
| Marketing | 3 | 4 | 5 | 7 | 9 | 10 | 12 | 16 |
| Customer Support | 1 | 3 | 5 | 7 | 16 | 26 | 37 | 48 |
| Compliance | 1 | 3 | 5 | 8 | 15 | 20 | 32 | 39 |
| Internal Control & Risks | - | - | 0 | 2 | 3 | 4 | 6 | 7 |
Forecast income statement of DOMINO SAS (central scenario), amounts in EUR:
| Line item | N-4 2020 & 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Payment activity revenue (Agent then PI) | 77,863 | 353,207 | 1,097,745 | 1,733,151 | 6,978,745 | 13,482,591 | 23,795,818 | 37,569,553 | 56,866,068 |
| Intermediation activity revenue | - | - | 4,323 | 135,188 | 1,220,774 | 2,629,880 | 4,858,312 | 8,954,863 | 15,273,144 |
| Total revenue | 77,863 | 353,207 | 1,102,068 | 1,868,339 | 8,199,518 | 16,112,470 | 28,654,130 | 46,524,416 | 72,139,212 |
| Banking system | - | 167,631 | 509,418 | 1,086,207 | 2,506,626 | 2,630,454 | 4,242,562 | 6,059,835 | 8,330,268 |
| Personnel expenses | 270,673 | 467,167 | 1,075,013 | 1,715,287 | 2,724,410 | 4,567,365 | 6,457,044 | 9,358,539 | 12,169,455 |
| Marketing & Acquisition | - | 69,555 | 264,254 | 788,362 | 3,059,791 | 5,148,410 | 9,017,222 | 15,065,892 | 26,539,640 |
| Other expenses | 177,000 | 303,036 | 451,786 | 704,429 | 2,184,241 | 2,959,777 | 4,282,157 | 6,452,433 | 9,435,199 |
| Depreciation, amortization and provisions (DAP) | - | 46,750 | 121,448 | 207,687 | 462,192 | 698,824 | 943,104 | 1,252,948 | 1,626,022 |
| Total expenses | 447,673 | 1,054,139 | 2,421,919 | 4,501,972 | 10,937,259 | 16,004,830 | 24,942,089 | 38,189,648 | 58,100,584 |
| Operating result | -369,810 | -700,931 | -1,319,851 | -2,633,633 | -2,737,741 | 107,641 | 3,712,042 | 8,334,768 | 14,038,628 |
| Financial income | - | - | - | 92,750 | 160,000 | 102,000 | 56,000 | 70,000 | 70,000 |
| Financial expenses | - | - | - | 44,295 | 103,660 | 77,654 | 68,439 | 52,348 | 35,684 |
| Financial result | - | - | - | 48,455 | 56,341 | 24,346 | -12,439 | 17,652 | 34,316 |
| Corporate income tax | (92,453) | (175,233) | (329,963) | (646,295) | (670,350) | 32,997 | 924,901 | 1,131,710 | 3,518,236 |
| Tax rate | 25% | 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) | 0 | 0 |
| Other taxes (CII, CIR) | 35,675 | 69,928 | 80,000 | 90,000 | 100,000 | - | - | - | - |
| Net result after tax | -334,135 | -777,723 | -1,239,851 | -2,495,178 | -2,581,401 | 131,987 | 3,699,603 | 7,220,710 | 10,554,708 |
| Cumulative result | -334,135 | -1,111,858 | -2,351,709 | -4,846,887 | -7,428,287 | -7,296,301 | -3,596,698 | 3,624,012 | 14,178,720 |
Forecast balance sheet of DOMINO SAS (central scenario), amounts in EUR:
| Asset line item | N-4 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Intangible fixed assets | 6,809 | 312,672 | 378,042 | 680,104 | 1,167,209 | 1,547,295 | 2,017,075 | 2,546,918 | 3,141,146 |
| Tangible fixed assets | - | 22,511 | 18,500 | 20,287 | 85,165 | 132,519 | 159,965 | 228,061 | 306,122 |
| Equity interests and related receivables | - | - | - | - | - | - | - | - | - |
| Other financial fixed assets | 133 | 30,140 | 30,140 | 30,140 | 3,294,995 | 5,947,803 | 9,837,121 | 14,941,923 | 21,682,800 |
| Fixed assets | 6,942 | 365,323 | 426,682 | 730,531 | 4,547,369 | 7,627,617 | 12,014,161 | 17,716,902 | 25,130,068 |
| Stocks | - | - | - | - | - | - | - | - | - |
| Operating receivables | - | 11,280 | - | - | - | - | - | - | - |
| Other receivables | 62,982 | 186,584 | 241,757 | - | - | - | - | - | - |
| Intercompany receivables | - | - | - | - | - | - | - | - | - |
| VAT and other taxes | - | - | 80,000 | 90,000 | 100,000 | - | - | - | - |
| Cash and cash equivalents | 204,975 | 526,508 | 4,740,890 | 2,452,928 | 9,661,620 | 7,555,922 | 7,767,126 | 12,099,383 | 20,216,713 |
| Prepaid expenses | 78,944 | 38,587 | 71,947 | - | - | - | - | - | - |
| Current assets | 346,901 | 762,959 | 5,134,594 | 2,542,928 | 9,761,620 | 7,555,922 | 7,767,126 | 12,099,383 | 20,216,713 |
| Total assets | 353,843 | 1,128,282 | 5,561,276 | 3,273,459 | 14,308,989 | 15,183,539 | 19,781,287 | 29,816,284 | 45,346,781 |
| Liability line item | N-4 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Share capital | 17,632 | 17,632 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 |
| Share premiums | 518,225 | 1,305,391 | 6,171,961 | 6,171,961 | 19,671,961 | 19,671,961 | 19,671,961 | 19,671,961 | 19,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,189 | 3,987,678 | 7,568,617 | 11,248,900 |
| Investment grant | - | 128,370 | - | - | - | - | - | - | - |
| Equity | 200,788 | 338,601 | 3,709,128 | 1,197,150 | 12,133,780 | 12,386,969 | 16,374,647 | 23,943,264 | 35,192,164 |
| Provisions for risks and charges | - | - | - | - | - | - | - | - | - |
| Financial debts | 92,214 | 489,047 | 1,660,000 | 1,613,125 | 1,340,000 | 1,215,000 | 835,000 | 595,000 | 305,000 |
| Short-term financial debts | - | - | - | - | - | - | - | - | - |
| Suppliers | 16,391 | 157,573 | 136,805 | 395,481 | 713,260 | 1,390,361 | 2,308,140 | 3,587,997 | 5,653,358 |
| Other debts | 500 | 18,125 | 55,343 | 67,702 | 121,949 | 191,210 | 263,500 | 350,508 | 446,625 |
| VAT and other taxes | 43,950 | 124,836 | - | - | - | - | - | 1,339,515 | 3,749,633 |
| Deferred income | - | - | - | - | - | - | - | - | - |
| Total liabilities | 353,843 | 1,128,182 | 5,561,276 | 3,273,459 | 14,308,989 | 15,183,539 | 19,781,287 | 29,816,284 | 45,346,781 |
Degraded Scenario
The degraded scenario is based on the following assumptions, expressed as differences from the central scenario:
-
Lower customer acquisition than expected, with -25% account-opening requests per year for all targeted customer segments;
-
Sharp increase in customer acquisition costs (+25%);
-
Significant decrease in the number of life-insurance contracts by -25%, with an acquisition cost of +25%;
-
16% decrease in the average number of transactions per month per card;
-
12% decrease in the average card transaction value.
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:
-
Staffing: adjustment of team size, partially variable according to activity evolution, with a total decrease of 14 FTEs on an annual-average basis, 20 at the end of N5, mainly in the Customer Service and Compliance teams, without however decreasing service quality or efforts regarding regulatory compliance;
-
Expenses directly linked to activity (COGS): highly variable, they will adjust to a lower activity level than anticipated.
G&A, Marketing & Communication, Tools & IT expenses will be maintained at the central-scenario level in order to preserve:
-
Efforts in customer acquisition through Marketing;
-
Development and provision of new functionalities in a potentially more competitive environment;
-
Strengthening of internal tools, their robustness and IT security compliant with market standards and applicable regulation;
-
The risk management and internal control system, including delegation of periodic control and operational risk management tools.
The investment assumptions linked to DOMINO SAS’s activity amount to a total of EUR 480K and break down as follows:
-
EUR 50K linked to the SEPA sub-participation project, with Arkéa as lead institution;
-
EUR 260K in Mastercard Cloud Edge set-up fees for Card Processing;
-
EUR 40K in set-up fees with card manufacturer and personalizer Exceet;
-
EUR 130K for registration of DOMINO SAS and the Green-Got program with Mastercard for MDES, mobile payment.
DOMINO SAS customer acquisition assumptions:
DOMINO SAS customer projections (degraded scenario), end of period in K Customers:
| Customer segment | N-4 2020 & 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Total customers / segment total | - | 10,077 | 21,525 | 30,350 | 57,073 | 82,865 | 118,096 | 160,402 | 212,536 |
| Individual Customer - Standard | - | 10,077 | 20,597 | 25,451 | 38,523 | 50,075 | 64,275 | 81,496 | 102,633 |
| Micro-Enterprise - Standard | - | - | 928 | 2,009 | 4,892 | 8,023 | 12,518 | 18,483 | 26,610 |
| Individual Customer - Premium | - | - | - | 2,205 | 8,162 | 13,404 | 19,515 | 26,597 | 35,005 |
| Micro-Enterprise - Premium | - | - | - | 685 | 2,462 | 4,513 | 7,522 | 11,447 | 16,720 |
| Enterprise - Standard | - | - | - | - | 2,263 | 5,097 | 10,684 | 16,739 | 23,560 |
| Enterprise - Premium | - | - | - | - | 771 | 1,753 | 3,583 | 5,639 | 8,008 |
DOMINO SAS’s revenue model:
Forecast evolution of DOMINO SAS revenue (degraded scenario):
| Revenue line | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total revenue | EUR 353,207 | EUR 1,179,750 | EUR 1,747,012 | EUR 6,836,757 | EUR 12,795,204 | EUR 22,329,911 | EUR 35,663,433 | EUR 54,753,281 |
| Subscriptions | EUR 296,585 | EUR 933,010 | EUR 1,306,655 | EUR 4,595,766 | EUR 7,687,078 | EUR 13,055,871 | EUR 19,718,926 | EUR 29,350,122 |
| Other payment-account revenue | EUR 6,933 | EUR 27,034 | EUR 33,459 | EUR 115,515 | EUR 173,156 | EUR 268,059 | EUR 393,051 | EUR 536,028 |
| Interchange | EUR 49,690 | EUR 215,383 | EUR 271,711 | EUR 1,131,476 | EUR 2,919,143 | EUR 5,310,880 | EUR 8,742,749 | EUR 13,091,863 |
| Investment revenue | EUR - | EUR 4,323 | EUR 135,188 | EUR 993,999 | EUR 2,015,827 | EUR 3,695,100 | EUR 6,808,707 | EUR 11,775,268 |
DOMINO SAS staffing:
Forecast evolution of DOMINO SAS staffing (degraded scenario), end of period:
| FTE category | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total FTEs, end of period | 16 | 27 | 35 | 55 | 75 | 105 | 130 | 153 |
| Admin & support functions | 1 | 3 | 3 | 6 | 9 | 13 | 15 | 16 |
| IT & Product | 7 | 10 | 12 | 16 | 22 | 26 | 31 | 34 |
| Marketing | 4 | 5 | 6 | 8 | 9 | 11 | 13 | 16 |
| Customer Support | 2 | 5 | 5 | 11 | 16 | 30 | 35 | 43 |
| Compliance | 2 | 4 | 6 | 11 | 16 | 20 | 30 | 38 |
| Internal Control & Risks | - | - | 3 | 3 | 3 | 5 | 6 | 6 |
Forecast evolution of DOMINO SAS staffing (degraded scenario), annual average:
| FTE category | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|
| Total FTEs, annual average | 11 | 20 | 28 | 40 | 67 | 89 | 121 | 144 |
| Admin & support functions | 1 | 2 | 3 | 4 | 8 | 11 | 15 | 16 |
| IT & Product | 4 | 8 | 11 | 13 | 19 | 24 | 28 | 33 |
| Marketing | 3 | 4 | 5 | 7 | 9 | 10 | 12 | 16 |
| Customer Support | 1 | 3 | 5 | 7 | 14 | 23 | 32 | 39 |
| Compliance | 1 | 3 | 5 | 8 | 14 | 17 | 28 | 34 |
| Internal Control & Risks | - | - | 0 | 2 | 3 | 4 | 5 | 6 |
Forecast income statement of DOMINO SAS (degraded scenario), amounts in EUR:
| Line item | N-4 2020 & 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Payment activity revenue (Agent then PI) | 77,863 | 353,207 | 1,175,427 | 1,611,824 | 5,842,757 | 10,779,377 | 18,634,810 | 28,854,726 | 42,978,013 |
| Intermediation activity revenue | - | - | 4,323 | 135,188 | 993,999 | 2,015,827 | 3,695,100 | 6,808,707 | 11,775,268 |
| Total revenue | 77,863 | 353,207 | 1,179,750 | 1,747,012 | 6,836,757 | 12,795,204 | 22,329,911 | 35,663,433 | 54,753,281 |
| Banking system | - | 167,631 | 562,757 | 1,046,453 | 2,228,265 | 2,058,127 | 3,322,521 | 4,734,625 | 6,308,645 |
| Personnel expenses | 270,673 | 467,167 | 1,075,013 | 1,715,287 | 2,684,988 | 4,362,171 | 5,976,537 | 8,731,627 | 10,983,186 |
| Marketing & Acquisition | - | 69,555 | 399,254 | 773,258 | 2,899,219 | 4,719,087 | 8,144,239 | 13,494,961 | 23,995,646 |
| Other expenses | 177,000 | 303,036 | 451,786 | 664,450 | 2,002,227 | 2,689,022 | 3,884,974 | 5,871,618 | 8,580,824 |
| Depreciation, amortization and provisions (DAP) | - | 46,750 | 121,448 | 224,487 | 460,541 | 684,841 | 934,086 | 1,239,932 | 1,565,092 |
| Total expenses | 447,673 | 1,054,139 | 2,610,257 | 4,423,936 | 10,275,239 | 14,513,248 | 22,262,357 | 34,072,763 | 51,433,394 |
| Operating result | -369,810 | -700,931 | -1,430,508 | -2,676,924 | -3,438,482 | -1,718,045 | 67,554 | 1,590,669 | 3,319,887 |
| Financial income | - | - | - | 92,750 | 160,000 | 102,000 | 56,000 | 70,000 | 70,000 |
| Financial expenses | - | - | - | 44,295 | 103,660 | 77,654 | 68,439 | 52,348 | 35,684 |
| Financial result | - | - | - | 48,455 | 56,341 | 24,346 | -12,439 | 17,652 | 34,316 |
| Corporate income tax | (92,453) | (175,233) | (357,627) | (657,117) | (845,535) | (423,425) | 13,779 | 402,080 | 838,551 |
| Tax rate | 25% | 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,675 | 69,928 | 80,000 | 90,000 | 100,000 | - | - | - | - |
| Net result after tax | -334,135 | -777,723 | -1,350,508 | -2,538,469 | -3,282,142 | -1,693,699 | 55,115 | 1,608,321 | 3,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 item | N-4 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Intangible fixed assets | 6,809 | 312,672 | 378,042 | 680,104 | 1,167,209 | 1,547,295 | 2,017,075 | 2,546,918 | 3,141,146 |
| Tangible fixed assets | - | 22,511 | 18,500 | 20,287 | 83,514 | 118,536 | 150,947 | 215,046 | 261,993 |
| Equity interests and related receivables | - | - | - | - | - | - | - | - | - |
| Other financial fixed assets | 133 | 30,140 | 30,140 | 30,140 | 2,379,229 | 4,152,860 | 6,657,614 | 9,771,334 | 13,673,543 |
| Fixed assets | 6,942 | 365,323 | 426,682 | 730,531 | 3,629,951 | 5,818,691 | 8,825,636 | 12,533,297 | 17,076,681 |
| Stocks | - | - | - | - | - | - | - | - | - |
| Receivables | 62,982 | 197,864 | 197,864 | - | - | - | - | - | - |
| VAT and other taxes | - | - | 80,000 | 90,000 | 100,000 | - | - | - | - |
| Cash and cash equivalents | 204,975 | 526,508 | 4,790,377 | 15,925,245 | 9,770,551 | 6,340,260 | 3,837,396 | 2,596,546 | 2,903,971 |
| Prepaid expenses | 78,944 | 38,587 | 71,947 | - | - | - | - | - | - |
| Current assets | 346,901 | 762,959 | 5,140,188 | 16,015,245 | 9,870,551 | 6,340,260 | 3,837,396 | 2,596,546 | 2,903,971 |
| Total assets | 353,843 | 1,128,282 | 5,566,870 | 16,745,776 | 13,500,503 | 12,158,951 | 12,663,032 | 15,129,843 | 19,980,652 |
| Liability line item | N-4 2021 | N-3 2022 | N-2 2023 | N-1 2024 | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|---|---|
| Share capital | 17,632 | 17,632 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 | 24,810 |
| Share premiums | 518,225 | 1,305,391 | 6,171,961 | 19,671,961 | 19,671,961 | 19,671,961 | 19,671,961 | 19,671,961 | 19,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,115 | 1,608,321 | 3,354,203 |
| Investment grant | - | 128,370 | - | - | - | - | - | - | - |
| Equity | 200,788 | 338,601 | 3,733,471 | 14,695,002 | 11,412,860 | 9,719,162 | 9,774,277 | 11,382,598 | 14,736,801 |
| Provisions for risks and charges | - | - | - | - | - | - | - | - | - |
| Financial debts | 92,214 | 489,047 | 1,641,250 | 1,613,125 | 1,300,000 | 1,145,000 | 775,000 | 595,000 | 305,000 |
| Short-term financial debts | - | - | - | - | - | - | - | - | - |
| Suppliers | 16,391 | 157,573 | 136,805 | 369,946 | 667,349 | 1,120,909 | 1,867,153 | 2,827,127 | 4,539,049 |
| Other debts | 500 | 18,125 | 55,343 | 67,702 | 120,293 | 173,880 | 246,604 | 325,118 | 399,802 |
| VAT and other taxes | 43,950 | 124,836 | - | - | - | - | - | - | - |
| Deferred income | - | - | - | - | - | - | - | - | - |
| Total liabilities | 353,843 | 1,128,182 | 5,566,870 | 16,745,776 | 13,500,503 | 12,158,951 | 12,663,033 | 15,129,843 | 19,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:
-
Method A ☐
-
Method B ☒
-
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:
-
Identification of the share of revenue (%) linked to payment activities = A%;
-
Identification of the share of revenue (%) linked to other activities (hybridity) = 1-A%;
-
Calculation of the share of the Balance Sheet corresponding to these other activities = (1-A%) * DOMINO SAS balance sheet total;
-
Application of a 15% coefficient to obtain the amount of expected additional Own Funds = 15% * (1-A%) * DOMINO SAS balance sheet total.
Own funds are composed as follows:
-
Capital;
-
Retained earnings;
-
Net result, taking results into account only once audited.
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):
| Scenario | Method | Metric | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|
| Central | Method A | Own funds requirement | EUR 1,091,923 | EUR 1,588,363 | EUR 2,465,401 | EUR 3,763,394 | EUR 5,717,499 |
| Central | Method A | Hybrid activity requirement | EUR 319,556 | EUR 371,739 | EUR 503,088 | EUR 860,841 | EUR 1,440,107 |
| Central | Method A | Total own funds requirement | EUR 1,411,479 | EUR 1,960,102 | EUR 2,968,489 | EUR 4,624,234 | EUR 7,157,606 |
| Central | Method A | Own funds after deduction | EUR 13,529,941 | EUR 10,586,485 | EUR 10,369,894 | EUR 13,827,729 | EUR 20,802,119 |
| Central | Method A | Own funds surplus / deficit | EUR 12,118,463 | EUR 8,626,384 | EUR 7,401,404 | EUR 9,203,495 | EUR 13,644,512 |
| Central | Method B | Own funds requirement | EUR 668,038 | EUR 1,594,932 | EUR 2,135,932 | EUR 2,592,042 | EUR 3,206,646 |
| Central | Method B | Hybrid activity requirement | EUR 319,556 | EUR 371,739 | EUR 503,088 | EUR 860,841 | EUR 1,440,107 |
| Central | Method B | Total own funds requirement | EUR 987,594 | EUR 1,966,671 | EUR 2,639,020 | EUR 3,452,882 | EUR 4,646,753 |
| Central | Method B | Own funds after deduction | EUR 13,529,941 | EUR 10,586,485 | EUR 10,369,894 | EUR 13,827,729 | EUR 20,802,119 |
| Central | Method B | Own funds surplus / deficit | EUR 12,542,348 | EUR 8,619,814 | EUR 7,730,873 | EUR 10,374,847 | EUR 16,155,365 |
| Central | Method C | Own funds requirement | EUR 2,818,725 | EUR 3,208,955 | EUR 3,827,749 | EUR 4,277,087 | EUR 4,752,991 |
| Central | Method C | Hybrid activity requirement | EUR 319,556 | EUR 371,739 | EUR 503,088 | EUR 860,841 | EUR 1,440,107 |
| Central | Method C | Total own funds requirement | EUR 3,138,281 | EUR 3,580,694 | EUR 4,330,837 | EUR 5,137,927 | EUR 6,193,098 |
| Central | Method C | Own funds after deduction | EUR 13,529,941 | EUR 10,586,485 | EUR 10,369,894 | EUR 13,827,729 | EUR 20,802,119 |
| Central | Method C | Own funds surplus / deficit | EUR 10,391,661 | EUR 7,005,791 | EUR 6,039,056 | EUR 8,689,802 | EUR 14,609,020 |
Calculation of Own Funds requirements according to Methods A, B & C (degraded scenario):
| Scenario | Method | Metric | N1 2025 | N2 2026 | N3 2027 | N4 2028 | N5 2029 |
|---|---|---|---|---|---|---|---|
| Degraded | Method A | Own funds requirement | EUR 1,027,524 | EUR 1,451,325 | EUR 2,226,236 | EUR 3,407,276 | EUR 5,143,339 |
| Degraded | Method A | Hybrid activity requirement | EUR 294,427 | EUR 287,338 | EUR 314,317 | EUR 433,279 | EUR 644,557 |
| Degraded | Method A | Total own funds requirement | EUR 1,321,951 | EUR 1,738,663 | EUR 2,540,553 | EUR 3,840,555 | EUR 5,787,897 |
| Degraded | Method A | Own funds after deduction | EUR 13,527,793 | EUR 9,865,566 | EUR 7,702,087 | EUR 7,227,359 | EUR 8,241,452 |
| Degraded | Method A | Own funds surplus / deficit | EUR 12,205,843 | EUR 8,126,903 | EUR 5,161,534 | EUR 3,386,804 | EUR 2,453,555 |
| Degraded | Method B | Own funds requirement | EUR 538,468 | EUR 1,314,086 | EUR 1,801,101 | EUR 2,185,632 | EUR 2,585,502 |
| Degraded | Method B | Hybrid activity requirement | EUR 294,427 | EUR 287,338 | EUR 314,317 | EUR 433,279 | EUR 644,557 |
| Degraded | Method B | Total own funds requirement | EUR 832,894 | EUR 1,601,424 | EUR 2,115,419 | EUR 2,618,911 | EUR 3,230,059 |
| Degraded | Method B | Own funds after deduction | EUR 13,527,793 | EUR 9,865,566 | EUR 7,702,087 | EUR 7,227,359 | EUR 8,241,452 |
| Degraded | Method B | Own funds surplus / deficit | EUR 12,694,899 | EUR 8,264,142 | EUR 5,586,668 | EUR 4,608,448 | EUR 5,011,393 |
| Degraded | Method C | Own funds requirement | EUR 2,750,565 | EUR 3,046,763 | EUR 3,518,089 | EUR 4,015,642 | EUR 4,439,340 |
| Degraded | Method C | Hybrid activity requirement | EUR 294,427 | EUR 287,338 | EUR 314,317 | EUR 433,279 | EUR 644,557 |
| Degraded | Method C | Total own funds requirement | EUR 3,044,992 | EUR 3,334,101 | EUR 3,832,406 | EUR 4,448,920 | EUR 5,083,898 |
| Degraded | Method C | Own funds after deduction | EUR 13,527,793 | EUR 9,865,566 | EUR 7,702,087 | EUR 7,227,359 | EUR 8,241,452 |
| Degraded | Method C | Own funds surplus / deficit | EUR 10,482,801 | EUR 6,531,465 | EUR 3,869,681 | EUR 2,778,439 | EUR 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:
-
The Chief Financial Officer of DOMINO SAS;
-
The Head of Payment Operations of DOMINO SAS.
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:
- The Supervisory Board (Conseil de Surveillance) is shown as in progress and linked to the Statutory Auditor (Commissaire aux Comptes), also in progress.
- Periodic Control (Contrôle Périodique) is shown as in progress.
- The Management Committee (Comité de Direction) includes:
- President and effective manager (dirigeant effectif): Maud CAILLAUX;
- Chief Executive Officer / General Manager and effective manager: Andréa GANOVELLI;
- Chief Technology Officer: Fabien HUET;
- Director of Internal Control: Kaïss BOUSRY.
- Externalized functions, including PSEE, are shown outside the management committee perimeter.
As indicated in Section 4, the governance of the Payment Institution will be composed of the following functions/bodies:
-
A Supervisory Board - see Section 4.7;
-
Two effective managers (dirigeants effectifs) - see Section 4.6:
-
Ms Maud CAILLAUX - President and Chief Marketing Officer of Green-Got, responsible for marketing, communication and customer acquisition;
-
Mr Andréa GANOVELLI - General Manager and Chief Executive Officer of Green-Got, responsible for supervising the finance, HR, customer relations, outsourced services and operations departments.
-
-
A Strategic Committee, composed of three members:
-
Ms Maud CAILLAUX;
-
Mr Andréa GANOVELLI;
-
Ms Heidi LINDVALL, General Partner of Pale Blue Dot.
-
-
A Management Committee, composed of four members:
-
Ms Maud CAILLAUX;
-
Mr Andréa GANOVELLI;
-
Mr Fabien HUET, Chief Technology Officer, responsible for IT and IS Security and responsible for payment operations;
-
Mr Kaïss BOUSRY, Head of Risk and Internal Control, responsible for risks and internal control.
-
-
An Audit and Risk Committee, composed of four members:
-
Mr Andréa GANOVELLI;
-
Mr Fabien HUET;
-
Mr Kaïss BOUSRY;
-
The Head of Compliance, recruitment in progress.
-
Organization and Organization Chart of DOMINO SAS
Visual summary of image62.png:
| Governance / reporting area | Roles and teams shown |
|---|---|
| Supervisory Board | Top-level body above the Strategic Committee, Management Committee, and Audit and Risk Monitoring Committee |
| Management | A. GANOVELLI, General Manager / CEO, Effective Manager 1; M. CAILLAUX, President / CMO, Effective Manager 2; F. HUET, CTO |
| Risk and Internal Control | K. BOUSRY, Head of Risk & Internal Control, shown alongside the management structure |
| Compliance | Head of Compliance, recruitment in progress; 2 FTE Compliance Manager; 3 FTE Compliance analyst |
| Customer Care & Ops | A. THOMAS; 1 FTE Customer Care Manager Investment Specialist; 1 FTE Customer Care Investment Specialist; 3 FTE Customer Care operator |
| Support functions | A. CRASATO, People Officer; 1 FTE CFO; A. BAULARD, Impact Officer |
| Growth | C. JAUNAULT; 1 FTE CRM Manager; 1 FTE Growth Engineer |
| Content | C. LeFranc; 1 FTE Content; 1 FTE Community Manager; 1 FTE Content & design |
| Product Management / Design | 1 FTE Product Manager; 1 FTE Product Designer; 1 FTE Assistant Product Manager |
| Development | 2 FTE mobile app development; 5 FTE back-end development |
Under the responsibility and direct management of Mr A. GANOVELLI, General Manager and Effective Manager:
-
The Compliance department (Compliance function / Fonction conformité), composed as follows:
-
To date: 1 Head of Risk & Compliance (K. Bousry), 3 FTE Compliance Manager and 2 Compliance Analyst apprentices. Managers and analysts are currently generalists but will begin specializing by customer type from 2024.
-
At end-2024: 6 FTEs, including a Head of Compliance to be recruited in place of Mr Bousry, who will take the position of Head of Risk and Internal Control upon authorization.
-
At end-2025: 12 FTEs, including the Head of Compliance.
-
At end-2026: 16 FTEs, including the Head of Compliance.
-
-
The Customer Care department (Customer Service Function), composed as follows:
-
To date: 1 FTE Head of Customer Care & Operations (A. Thomas), 3 FTE Senior Customer Care operators and 1 Junior Customer Care operator apprentice. Customer care operators are currently generalists but will begin specializing by customer type from 2024.
-
At end-2024: 6 FTEs, including the Head of Customer Care & Operations.
-
At end-2025: 11 FTEs, including the Head of Customer Care & Operations.
-
At end-2026: 21 FTEs, including the Head of Customer Care & Operations.
-
-
Support functions:
-
HR Function:
-
To date: 1 FTE.
-
At end-2024: 2 FTEs.
-
At end-2025 and end-2026: 3 FTEs.
-
-
Finance Function:
-
To date: 1 apprentice.
-
At end-2024 and end-2025: 1 FTE.
-
At end-2026: 2 FTEs.
-
-
Impact Function:
-
To date and at end-2024: 1 FTE.
-
At end-2025 and end-2026: 2 FTEs.
-
-
-
Outsourced functions - see Section 7.2.
Under the responsibility and direct management of Ms M. CAILLAUX, President and Effective Manager:
-
The Growth department, composed as follows:
-
To date: 2 FTEs and one apprentice;
-
At end-2024: 2 FTEs;
-
At end-2025 and end-2026: 4 FTEs.
-
-
The Content department, composed as follows:
-
To date: 2 FTEs and one apprentice;
-
At end-2024 and end-2025: 3 FTEs;
-
At end-2026: 4 FTEs.
-
-
Outsourced functions - see Section 7.2.
Under the responsibility and direct management of Mr F. HUET, CTO:
-
The Product Management / Design department, composed as follows:
-
To date: 2 FTEs and one apprentice;
-
At end-2024: 2 FTEs;
-
At end-2025: 3 FTEs;
-
At end-2026: 4 FTEs.
-
-
The IT department, composed as follows:
-
To date: 7 FTEs;
-
At end-2024: 8 FTEs;
-
At end-2025: 12 FTEs;
-
At end-2026: 16 FTEs.
-
-
Outsourced functions - see Section 7.2.
Under the responsibility and direct management of Mr K. BOUSSRY, Head of Risks & Internal Control:
-
The Risks and Internal Control department, composed as follows:
-
To date: this team does not exist; the risk and internal control function is currently still operated by the Compliance Function. To distinguish operational functions from control functions and comply with the applicable regulation, DOMINO SAS wishes to establish a dedicated team.
-
At end-2024: 1 FTE.
-
At end-2025: 3 FTEs.
-
At end-2026: 4 FTEs.
-
Use of Outsourcing
Visual summary of image13.png:
| Area shown in visual | Services / 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 functions | Onboarding; 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 services | SEPA lead institution: Arkéa; safeguarding account (Compte de Cantonnement): Arkéa |
| Other non-essential services | Payroll 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:
-
SEPA Lead Bank services with Arkéa;
-
Identity verification of Natural Person customers with Ubble;
-
Customer screening for politically exposed persons (personnes politiquement exposées, PPE) and asset freezes (gel des avoirs, GDA) with Efficiale;
-
Transaction monitoring with HAWK:AI;
-
Database hosting with AWS, covering customer data and business management data;
-
Periodic control, with partner selection in progress;
-
Production, personalization and shipping of Cards with Exceet;
-
Acquisition and processing of card payments used to fund Accounts with Stripe;
-
Production and follow-up of regulatory reporting with Invoke.
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:
-
SEPA Lead Bank services: under the responsibility of the CTO;
-
Customer identity verification: under the responsibility of the Head of Compliance;
-
Customer screening for PPE and GDA: under the responsibility of the Head of Compliance;
-
Transaction monitoring: under the responsibility of the Head of Compliance;
-
Data hosting: under the responsibility of the CTO;
-
Periodic control: under the responsibility of the President;
-
Production, personalization and shipping of Cards: under the responsibility of the Head of Customer Care;
-
Acquisition and processing of card payments used to fund Accounts: under the responsibility of the CTO;
-
Production and follow-up of regulatory reporting: under the responsibility of the Head of Risk & Internal Control.
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:
-
Payroll management: ECAI;
-
Accounting: ECAI;
-
Legal secretariat: Inlo Avocats;
-
Statutory auditor (Commissaire aux comptes, CAC): BDO;
-
Safeguarding of funds (cantonnement des fonds): Arkéa.
Participation in a National or International Payment System
DOMINO SAS will participate in the following payment systems:
-
SEPA, through its lead institution, Arkéa, a directly participating Credit Institution;
-
Mastercard, as Principal Member.
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:
-
Permanent control - Level 1:
-
Agents performing operational activities;
-
Line managers.
-
-
Permanent control - Level 2: the Head of Risks and Internal Control;
-
Periodic control - Level 3: the internal audit function, outsourced to a Firm being selected;
-
The Audit and Risk Committee;
-
The Effective Managers;
-
The Supervisory Board.
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:
-
Step 1: Identification of processes and associated risks;
-
Step 2: Assessment of risk exposure - definition of gross risks, i.e. risk without taking into account the existing control environment for controlling it, according to frequency and impact;
-
Step 3: Assessment of the effectiveness and adequacy of risk-control systems - definition of net risks;
-
Step 4: Treatment of net or residual risks: determine the acceptability limit and prepare an action plan.
4. Permanent and Periodic Control Plans Associated with the Risk Map
DOMINO SAS has prepared a permanent control plan listing the controls performed:
-
By operational staff (self-controls) and their management (controls targeted on the most sensitive points) in the processing of operations: Level 1;
-
By the Head of Risks and Internal Control: Level 2.
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:
-
Legal risks linked to governance: shareholders, members of the Supervisory Board, the President or the General Manager of the company do not comply with the organization, decision-making, administration and formalism provisions established by the Articles of Association or Shareholders’ Agreement;
-
Legal risks linked to Customer litigation: a litigation action is initiated by a Customer following services provided by DOMINO SAS that do not comply with its contractual obligations.
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:
-
Non-compliance with applicable and future regulatory obligations, including GDPR, PSD2, the 5th AML/CFT Directive, etc.;
-
Non-compliance with production deadlines for regulatory statements, Money Laundering / Protection of Users’ Interests questionnaires and annual internal control reports;
-
Non-compliance with internal procedures, such as procedures for safeguarding funds or AML/CFT / asset freeze;
-
Non-compliance with the rules of the internal control system and the applicable regulation on internal control, including the Order of 3 November 2014.
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:
-
Reputational risk;
-
Execution error risk;
-
Fraud risk, internal or external;
-
IT risk;
-
Business continuity disruption risk;
-
Natural disaster.
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:
- An insufficient level of own funds preventing DOMINO SAS from properly carrying out its activity.
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:
- 1 FTE, the Head of Risk and Internal Control.
The staffing dedicated to internal control will increase over the next 6 financial years of DOMINO SAS, in line with the projected workload, namely:
-
2025: 3 FTEs out of a total of 56 FTEs;
-
2029: 8 FTEs out of a total of 173 FTEs.
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:
-
Legal and regulatory provisions, in particular those specific to banking and financial activities;
-
Professional and ethical practices;
-
Internal rules and orientations defined by the Supervisory Board and implemented by the Effective Managers.
In addition, DOMINO SAS’s internal control and risk management system also makes it possible to verify:
-
That decision-making and risk-taking procedures, whatever their nature, and the management standards set, in particular in the form of limits by the Effective Managers within the policies and orientations of the supervisory body and defined in accordance with risk appetite, are strictly respected;
-
The quality of accounting and financial information and information relating to management standards, whether intended for the Effective Managers or the Supervisory Board, transmitted to the ACPR or included in documents intended for publication;
-
The conditions for evaluating, recording, retaining and making this information available, in particular by guaranteeing the existence of the audit trail;
-
The quality of processes contributing to the security and proper functioning of the information system and to business continuity;
-
The execution of decided corrective measures within reasonable timeframes.
DOMINO SAS’s internal control and risk management system is based on five fundamental principles:
-
Exhaustiveness of the control perimeter, covering all internal/external processes and all types of risks;
-
Individual responsibility of each employee and each manager in controlling risks;
-
Responsibility of the Head of Risks and Internal Control, based on expertise and independence;
-
Proportionality of controls to the scale of risks incurred;
-
Independence of periodic control.
DOMINO SAS’s internal control and risk management system is therefore composed of the following elements:
-
The risk map identifying the risks to which DOMINO SAS is exposed because of its activities, organization, tools, etc.;
-
The body of internal policies and procedures, including in particular those mentioned in the Order of 3 November 2014;
-
First- and second-level controls, performed in accordance with the Permanent Control Plan and archived, guaranteeing the existence of the audit trail;
-
Third-level controls, performed in accordance with the Periodic Control Plan and archived, guaranteeing the existence of the audit trail;
-
Training and awareness-raising for relevant staff on risk management, in accordance with Article 39 of the Order of 3 November 2014.
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:
-
Expression of needs formulated by Business Managers and validated by the Effective Managers (Dirigeants Effectifs);
-
Qualification of the contemplated service by the Head of Risks and Internal Control;
-
Selection of the partner by the Effective Managers, taking into account at least the following criteria:
-
Technical expertise;
-
Strength of financial, human and material resources;
-
Required approvals, authorizations and certifications;
-
Honorability, integrity and reputation.
-
-
Checks performed by the Head of Risks and Internal Control on the selected provider:
-
Analysis of operational risks;
-
Required approvals, authorizations and certifications;
-
Quality of the provider;
-
Emergency and Business Continuity Plan (Plan d’Urgence et de Poursuite d’Activité, PUPA);
-
Information-system security;
-
Confidentiality of information.
-
-
Contracting and verification that the draft contract satisfies the obligations set out in Articles 231 to 240 of the Order of 3 November 2014.
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:
-
Customer characteristics;
-
The nature of the products and services offered;
-
The transaction conditions proposed;
-
The distribution channels used;
-
The countries of origin or destination of funds.
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:
-
Natural-person customers who are adults and legally capable, resident in France or in another EU / EEA Member State;
-
Natural-person customers who are adults, resident in France or in another EU / EEA Member State, and acting for professional needs as auto-entrepreneurs;
-
Legal-person customers, commercial companies registered in France or in another EU / EEA Member State, whose Representative is an adult and legally capable natural person tax-resident in France or in another EU / EEA Member State.
DOMINO SAS systematically refuses any business relationship with:
-
A natural person who is a minor or legally incapable, such as an adult under guardianship;
-
Natural persons who do not reside in an EU / EEA Member State;
-
Natural persons who are nationals of a country on DOMINO’s blacklists / greylists;
-
Legal persons, associations or foundations;
-
Legal persons that are not registered and do not have their registered office in an EU / EEA Member State;
-
Legal persons whose Representative does not have the powers needed to act in the name and on behalf of the legal-person customer;
-
Legal persons whose Representative resides outside the EU / EEA and/or is a national of a country on DOMINO’s blacklists / greylists;
-
Legal persons one of whose beneficial owners resides in and/or is a national of a country on DOMINO’s blacklists / greylists;
-
Natural or legal persons linked to a country on DOMINO’s blacklists / greylists, such as activities linked to a blacklisted country or a legal-person customer’s parent company registered in a blacklisted country;
-
Natural or legal persons carrying out prohibited activities, as set out in the KYC Procedure;
-
Natural or legal persons subject to an asset-freezing measure (gel des avoirs);
-
Natural or legal persons suspected of and/or convicted for terrorism and related offences;
-
Natural or legal persons suspected of and/or convicted for deliberate violation or infringement of laws and regulations relating to financial crime;
-
U.S. persons within the meaning of the Foreign Account Tax Compliance Act (FATCA);
-
Natural or legal persons providing false or inaccurate information or falsified documents;
-
Natural or legal persons providing inconsistent information or documents containing inconsistent information;
-
Natural or legal persons refusing to provide the requested information or documents.
Risks Relating to Products and Services Offered
DOMINO SAS offers the following services:
-
Opening and maintenance of payment accounts, without overdraft facility;
-
Execution of payment transactions by card, incoming transfer and outgoing transfer;
-
Issuance of payment cards;
-
Cash withdrawals from a payment account.
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:
-
Use of the payment account, for example non-use of the account or use of inactive accounts concealing capital;
-
Characteristics of payment transactions, in particular:
-
For each cash-out payment transaction, including withdrawal, card payment, transfer or direct debit, and cash-in transaction, including card payment and transfer:
-
The unit amount of the transaction;
-
The total amount of transactions carried out by the same customer over rolling 7-day and 30-day periods;
-
The total amount of transactions carried out by the same customer to the same beneficiary over rolling 7-day and 30-day periods;
-
The total number of transactions carried out by the same customer over a rolling 14-day period;
-
The total number of transactions carried out by the same customer to the same beneficiary over a rolling 14-day period;
-
-
Whether transactions are unusually high in amount, having regard to the customer’s risk profile and transaction history;
-
The complexity of transactions;
-
Whether transactions appear to have no economic justification or apparent lawful purpose;
-
Whether transactions are initiated by a customer linked to a person or entity subject to an asset-freezing measure and/or a Politically Exposed Person (Personne Politiquement Exposée, PPE).
-
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:
-
For natural persons: verification of the customer’s identity using an identity-verification solution certified by ANSSI (PVID);
-
For legal persons: collection of a Kbis or foreign equivalent dated less than 3 months earlier, remote manual verification plus first payment, or collection of the Kbis directly from the registry.
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:
-
EU / EEA countries (risk 1): standard due diligence;
-
Equivalent non-EU / non-EEA countries (risk 2): enhanced due diligence;
-
Non-equivalent non-EU / non-EEA countries (risk 3): enhanced due diligence;
-
Prohibited countries (risk 4): the transaction is blocked and cannot be carried out. The transaction is prohibited.
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:
-
Determining strategic orientations for internal control, compliance and risk management;
-
Determining DOMINO SAS’s risk appetite, particularly for AML/CFT;
-
Determining the nature, volume, form and frequency of information transmitted to it on internal control and AML/CFT / asset freezing;
-
Reviewing, approving and adopting the AML/CFT Policy and the ML/TF Risk Classification, assessing their effectiveness and the corrective measures adopted, in particular on the basis of permanent and periodic control reports;
-
Reviewing, at least once a year, the activity report of the person responsible for Compliance and obtaining, at least once per quarter, provisional reports on activities exposing DOMINO SAS to higher ML/TF risks;
-
Conducting, at least twice a year, a review of internal-control activity and results, particularly in AML/CFT / asset freezing, based in particular on permanent control reports prepared by the Head of Risks and Internal Control and periodic control reports from the internal audit function, and monitoring DOMINO SAS’s risk appetite;
-
Controlling the coordination of all DOMINO SAS internal-control frameworks, including internal control of AML/CFT / asset-freezing frameworks.
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:
-
Setting annual objectives, particularly for AML/CFT / asset freezing;
-
Implementing an appropriate and effective organizational and operational structure to comply with the annual AML/CFT / asset-freezing objectives;
-
Issuing decisions and instructions for implementation of the AML/CFT Policy and the ML/TF risk classification defined by the Supervisory Board, including review, approval and adoption of applicable AML/CFT / asset-freezing procedures;
-
Conducting, at least twice a year, a review of internal-control activity and results, particularly in AML/CFT / asset freezing, based in particular on permanent control reports prepared by the Head of Risks and Internal Control and periodic control reports from the internal audit function, and monitoring DOMINO SAS’s risk appetite;
-
Taking the corrective measures necessary to remedy significant AML/CFT / asset-freezing incidents and deficiencies escalated to them, in particular through the internal-control framework;
-
Ensuring responsibility for the coherence and effectiveness of permanent and periodic controls over AML/CFT / asset-freezing frameworks;
-
Appointing the TRACFIN reporting officer(s) and correspondent(s);
-
Participating in the Audit and Risk Committee.
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:
-
Assisting the Supervisory Board in determining the risk appetite, particularly for AML/CFT;
-
Assisting the Supervisory Board in reviewing, approving and adopting the AML/CFT Policy and ML/TF Risk Classification, and assessing their effectiveness and the corrective measures adopted;
-
Validating onboarding of certain high-risk Customers;
-
Approving the annual activity report.
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:
-
Are carried out independently by persons dedicated solely to the transaction-control function; and
-
Have as their main objective ensuring that risks have been identified and managed by the first control level, in accordance with agreed rules and procedures.
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:
-
Customer identification;
-
Verification of customer identity;
-
Collection of business-relationship knowledge information;
-
Screening of Customers to identify whether they must be classified as Politically Exposed Persons (PPE) or whether they are subject to an asset-freezing measure;
-
Establishment of an individualized customer risk profile;
-
Implementation of additional due-diligence measures where onboarding a PPE Customer or a high-risk Customer;
-
Validation of onboarding into the business relationship.
During the business relationship:
-
Updating business-relationship knowledge;
-
Monitoring transactions / operations carried out by Customers;
-
Managing internal / external alerts;
-
Performing enhanced reviews in the presence of atypical transactions;
-
Filing suspicious-transaction reports with TRACFIN;
-
Filing reports with DG TRÉSOR.
These measures are described in detail in the operating policies and procedures included in the annex:
-
7.2.2 AML/CFT and Asset-Freezing Compliance Framework Policy (Politique cadre conformité de LCB-FT et de Gel des avoirs);
-
7.2.3 KYC Procedure;
-
7.2.4 Transaction Monitoring Procedure;
-
7.2.5 TRACFIN Reporting Procedure;
-
7.2.6 Asset-Freezing Procedure.
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:
- 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).
- Identity document
a. Passport (data and image);
b. Or identity card (data and image);
c. Or residence permit (data and image).
- Contact details
a. Full residential address;
b. Telephone number;
c. Email address;
d. Country of tax residence.
- 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.
- 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.
- Contact details
a. Proof of address;
b. Tax identification number.
- 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:
- 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.
- Tax identification
a. Tax identification number;
b. VAT number.
- 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.
- 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.
- 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:
-
Company articles of association;
-
Minutes of general meetings and other important legal documents;
-
Insurance certificates or other regulatory documents;
-
Company accounts;
-
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:
-
The public signing key generated by the customer;
-
The key creation date.
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):
-
sepa_details: contains information specific to SEPA transactions, such as the reference, purpose, account number and mandate identifier; -
counterpart: details of the beneficiary’s bank account, including account type, address, bank name, BIC/SWIFT and IBAN.
Data specific to card transactions:
-
card_details: contains details specific to card transactions, such as the card token and Mastercard transaction identifier; -
pos(point of sale), if applicable: details of the point-of-sale terminal, including location, card entry mode, presence of the cardholder, merchant ID and cardholder authentication method; -
ecom(e-commerce), if applicable: details relating to an online merchant for an e-commerce purchase.
Common data:
-
id,accounting_date,created_at,modified_at,executed_at: unique identifiers and dates associated with the transaction; -
attachments: documents attached to the transaction; -
beneficiary_type: type of beneficiary entity (company, natural person, etc.); -
category:label_clean,label_raw,mcc,mid- transaction enrichment information; -
change_rate: exchange rate, if applicable; -
co2_emission: information on CO2 emissions associated with the transaction; -
vat: VAT data, if applicable; -
transaction_direction: indicates whether the transaction is a credit or a debit; -
transaction_status: status of the transaction (for example, Completed, Pending); -
amount: transaction amount.
Monitoring data
To be able to monitor transactions, DOMINO SAS collects the following data:
-
For all operations:
-
Keywords assigned to movements (labels): Gold, rental, debt, etc.;
-
Destination of funds (country / bank for transfers, MCC code for card transactions);
-
Account-name / transaction-label matching;
-
Amount;
-
Splitting of operations;
-
Rapid inflows / outflows of funds for time-based analysis;
-
Time since the last transactions for dormant accounts.
-
-
For card transactions (e-commerce):
-
Number of attempts;
-
Website used (MID, MCC) for comparison with lists of problematic websites;
-
Use toward certain currencies;
-
3DS failures.
-
-
For withdrawals:
-
Withdrawals of an amount greater than 250 €;
-
Withdrawal frequency;
-
Withdrawal location.
-
-
For card transactions (POS):
-
Payment location for geographic analysis;
-
Number of attempts.
-
-
For all customers:
-
Login country via IP for geographic analysis;
-
Telephone operator;
-
Number of different telephones / computers used;
-
High balance;
-
Cross-checking of data with accounts already subject to an alert across all criteria.
-
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 Back Office: transcription of collected KYC data and the full transaction history;
-
Hawk.ai, the transaction monitoring platform: all data relating to transactions;
-
Metabase, a data management and extraction tool used to produce dashboards in order to monitor the activity of departments using the data (Marketing, Finance and Compliance).
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
-
Number of active accounts;
-
Number of accounts opened;
-
Number of account-opening attempts;
-
Number of rejected account openings;
-
Number of account openings in progress;
-
Breakdown of customers according to their risk level (number of LOW, MEDIUM and HIGH risk).
TRANSACTION MONITORING: weekly data
-
Total number of transactions;
-
Number of transactions analyzed;
-
Number of transactions under analysis;
-
Percentage of hits (ratio between analyzed transactions and total number of transactions);
-
Number of monthly hits on hawk.ai (transaction monitoring platform);
-
Breakdown of transactions (between cash withdrawals and SEPA transactions);
-
Breakdown of hits according to socio-professional categories (CSP) and income brackets;
-
Top 5 hit categories.
ACCOUNT MANAGEMENT: weekly data
-
Number of blocked accounts (in progress);
-
Number of closed accounts;
-
Number of SATD;
-
Breakdown of internal alerts (hawk.ai) and external alerts (suspicion reports);
-
Number of suspicious activity reports in progress;
-
Number of suspicious activity reports sent;
-
Breakdown of suspicious activities in progress (number of pass-through accounts, disbursements, significant debit / credit activity, etc.).
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:
-
Backup management is fully outsourced to AWS;
-
Backup frequency in replicas is continuous. 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:
-
A database state is saved every hour. Hourly states are retained for 24 hours. They may be used to restore a production database if the production database and all of its replicas fail simultaneously;
-
Daily backups are retained for 7 days;
-
Weekly backups are retained for 1 month.
-
-
Physical and logical security of network equipment is provided by AWS;
-
Security of logical servers is provided by the DOMINO SAS IT team;
-
The password management policy is based on a purely manual and oral process. DOMINO SAS remains a sufficiently lightweight structure for the three accredited persons (RSSI, Lead Ops and Lead Back) to directly provide OTP generator keys to the relevant persons during a video call or in person;
-
The access-rights management policy is based on a logical cascade in which the rights that users inherit from the groups to which they belong also include the list of groups they are able to administer.
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:
-
Through direct communication from users or DOMINO SAS personnel via online support;
-
Through external communication initiated by a third-party service provider that has repercussions for systems or customers;
-
Through a message initiated by an automated system that triggers an alert.
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:
-
Ensuring that all incidents are correctly recorded in the incident management software (incident.io);
-
Determining the nature of incidents based on reported symptoms and categorization rules provided by supplier groups;
-
Prioritizing incidents based on the impact on users at incident closure;
-
Delegating responsibility by assigning incidents to the appropriate supplier group for resolution, including third-party suppliers where applicable;
-
Performing a customer review after resolution to ensure that all work services are functioning properly and that all incident documentation is complete;
-
Preparing reports showing statistics on resolved / unresolved incidents and compliance with service agreements.
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:
-
An online form on the DOMINO SAS website (customer area);
-
A dedicated chat in the application;
-
The following telephone line: 01 89 71 02 07.
Customer Service will be the contact point for customers, in particular in the following cases (most frequent cases):
-
Unauthorized / unrecognized card payment transaction by a customer;
-
Phishing cases;
-
Unauthorized / unrecognized transfer;
-
Entry of data linked to the card or account on a fraudulent website.
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.
-
A chat available directly in the Green-Got application allows the customer to identify themselves and communicate in real time with a DOMINO SAS operator;
-
The customer may also contact DOMINO SAS Customer Service and/or Compliance through the shared generic email address (via the Intercom tool) used by DOMINO SAS Customer Service operators and Compliance operators;
-
Internally, the operators of these two functions communicate on a dedicated channel (via Slack) to qualify / analyze the problem raised by the customer and enable Compliance to determine the steps to be followed.
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:
-
DOMINO SAS Fraud / AML-CFT tools, used to build customer profiling, are the Back Office (developed internally) and Hawk.ai (external transaction analysis platform);
-
Internal dual-control procedures, applicable both at operations level and in IT matters, which make it possible in particular to detect cases of internal fraud.
The Head of Compliance may be alerted to suspicious activity in two ways:
-
Reporting by Customer Service (via email, an internal message on the dedicated “customer-care-x-compliance” channel, or a customer complaint);
-
Identification through the dedicated monitoring tool (the Back Office).
The Head of Compliance reviews:
-
Data associated with the transaction history: the Head of Compliance is able to assess the consistency between the transactions and KYC, and therefore to identify potential unusual transactions;
-
Login data: the Head of Compliance is able to identify unusual behavior (password changes, IP address responsible for logins to several customer accounts, etc.).
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:
-
Card-payment fraud cases;
-
Money laundering cases;
-
Hacking of the online account in the event of access-code theft;
-
Other scenarios that operators list and update through monitoring of new fraud practices.
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:
-
The Customer Service function;
-
The Fraud function;
-
The Administrative and Financial Department;
-
Operational teams.
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:
-
The Authorization Implementation and Monitoring Committee (IT Department Manager, functional managers for each application / scope, authorization manager and administrator, managers);
-
A single repository of physical or application users managed by the IT security administrator;
-
Segmentation of the single user repository by business sector and function;
-
A process for defining authorizations by application;
-
A process for granting and updating authorizations, allowing the user to be given access rights limited to only the data strictly necessary to perform their duties;
-
Traceability of access and actions performed by users;
-
Communication of the authorization management process to all users as part of awareness raising.
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:
-
Personal data linked to identity;
-
Data relating to payment instruments or the transaction;
-
Login data.
Operations or processes involving sensitive data:
| Operation | Description 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 data | Collection 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 account | Collection 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 Office | DOMINO 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 card | Creation 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 customer | When 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 data | When 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 challenge | During 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:
-
Either directly by the account holder, online via the application or website;
-
Or by a Customer Service advisor from the Back Office.
Personal data linked to identity are as follows:
-
Personal identification information:
-
Full name (first name and surname);
-
Date and place of birth;
-
Nationality;
-
Photo (via Ubble for PVID identification);
-
Video (via Ubble for PVID identification);
-
Tax identification number;
-
Risk scoring of the business relationship;
-
Politically exposed person status;
-
Presence on sanctions lists;
-
Passport (data and image) OR identity card (data and image) OR residence permit (data and image).
-
-
Contact details:
-
Full residential address;
-
Telephone number;
-
Email address;
-
Country of tax residence;
-
Proof of address.
-
-
Situation data:
-
Occupation;
-
Income level;
-
Income tax notice;
-
Property/local tax notice;
-
Property title.
-
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:
-
Customer.io: application responsible for sending emails to customers and managing the marketing relationship - a tool essential to the CRM;
-
FICOBA: National Register of Bank and Similar Accounts - lists all bank accounts opened in France: current accounts, savings accounts, securities accounts, etc.;
-
Exceet and Mastercard: for manufacturing, shipping and managing payment cards;
-
Hawk:AI: transaction monitoring requires contextual information on the sender (or receiver);
-
Arkéa: for the operation and receipt of SEPA transfers;
-
Generali and CFCAL: for feeding their own KYC - limited to DOMINO SAS customers who have subscribed to a product from one of these partners.
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:
-
Collection;
-
Recording;
-
Consultation;
-
Structuring;
-
Modification;
-
Use;
-
Transmission;
-
Retention;
-
Deletion.
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:
-
When authorization requests, clearing files and settlement files are received from Mastercard;
-
During data exchanges with Arkéa;
-
During transaction-data enrichment (categorization, CO2 cost calculation, label cleaning, logo addition, etc.).
Data relating to payment instruments or a transaction cover the following data types:
-
Data specific to SEPA transactions (Sct, SCT Inst, Sdd, Sdd B2B):
-
sepa_details: contains information specific to SEPA transactions, such as the reference, purpose, account number and mandate identifier; -
counterpart: details of the beneficiary’s bank account, including account type, address, bank name, BIC/SWIFT and IBAN.
-
-
Data specific to card transactions:
-
card_details: contains details specific to card transactions, such as the card token and Mastercard transaction identifier; -
pos(point of sale), if applicable: details of the point-of-sale terminal, including location, card entry mode, presence of the cardholder, merchant ID and cardholder authentication method; -
ecom(e-commerce), if applicable: details relating to an online merchant for an e-commerce purchase.
-
-
Common data:
-
id,accounting_date,created_at,modified_at,executed_at: unique identifiers and dates associated with the transaction; -
attachments: documents attached to the transaction; -
beneficiary_type: type of beneficiary entity (company, natural person, etc.); -
category:label_clean,label_raw,mcc,mid- transaction enrichment information; -
change_rate: exchange rate, if applicable; -
co2_emission: information on CO2 emissions associated with the transaction; -
vat: VAT data, if applicable; -
transaction_direction: indicates whether the transaction is a credit or a debit; -
transaction_status: transaction status (for example, Completed, Pending); -
amount: transaction amount.
-
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:
-
Mastercard: for executing transactions;
-
Hawk:AI: for transaction monitoring;
-
Arkéa: for operating and receiving SEPA transfers;
-
Generali and CFCAL: for managing savings accounts and life insurance policies.
Nature of possible operations on data relating to payment instruments or the transaction:
| Nature of operations | Customer applications | Back Office | DOMINO SAS servers and databases | Hawk:AI | Mastercard / Arkéa | Generali / CFCAL |
|---|---|---|---|---|---|---|
| Collection | Yes | Yes | Yes | Yes | Yes | Yes |
| Recording | Yes | Yes | Yes | Yes | ||
| Reading or consultation | Yes | Yes | Yes | Yes | Yes | Yes |
| Structuring | Yes | Yes | Yes | |||
| Modification | Yes | Yes | Yes | |||
| Use | Yes | Yes | Yes | |||
| Transmission | Yes | Yes | Yes | Yes | Yes | |
| Retention | Yes | Yes | Yes | Yes | ||
| Deletion | Yes | Yes | Yes |
Point of attention:
-
DOMINO SAS business processes run on the servers and DOMINO SAS financial partners are able to create transactions and benefit from rights over the processed data (audit);
-
Customer applications and the Back Office may only read, collect and transmit data to a server;
-
Hawk:AI must be able to retain data in order to process them in the future and perform monitoring and data analysis. The data made available are partially anonymized before transmission.
Login data
Login-related data are collected:
-
During the initial login to initialize session data;
-
At each API call to verify them.
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:
-
The user’s IP;
-
The OS;
-
The browser;
-
The device fingerprint;
-
The user agent.
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:
-
Either by the server with a key stored in AWS KMS;
-
Or by one of the three persons authorized to administer the production database (RSSI, Lead Back End, Lead Ops).
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:
-
The customer’s online customer account;
-
The GUIs and Back Offices of the various IS applications, by DOMINO SAS personnel according to associated roles and access rights;
-
The server, which directly accesses the database;
-
AWS GUIs.
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:
-
PAN;
-
PIN;
-
CVC;
-
Expiration;
-
Cardholder name;
-
Full IBAN.
Access to data by DOMINO SAS personnel:
DOMINO SAS personnel must apply the Authorization Management Policy in force:
-
The policy applies to all employees without exception, including when working remotely from home or while traveling, including in subsidiaries, and including external employees, subject to appropriate contractual commitments or provisions;
-
The policy applies to all types of hardware and software.
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.
| Function | Role in the process |
|---|---|
| RSSI | Validates 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 End | Directs and controls implementation of the business logic that restricts access for any person or service that is not authorized. |
| Lead Ops | Directs 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. |
| RCCI | Accesses 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:
-
The identifier is the user’s email;
-
External users are identified by an email that is not at
@green-got.com; -
The OTP generation key is transmitted orally during a videoconference call with the Lead Infrastructure.
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:
-
Extract data to generate a complete mapping of groups and their members in a single file;
-
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;
-
Compare these two files to verify that there are no discrepancies;
-
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:
-
A database state is saved every hour. Hourly states are retained for 24 hours. They may be used to restore a production database if the production database and all of its replicas fail simultaneously;
-
Daily backups are retained for 7 days;
-
Weekly backups are retained for 1 month.
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:
-
The CTO;
-
The Lead Back End;
-
The Lead Infrastructure.
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:
-
Create;
-
Delete;
-
Modify;
-
Replace;
-
Read.
Point of attention: only functionalities or operations relating to sensitive data are presented below, application by application.
Customer-accessible applications (web and mobile):
| Application role | Enable end customers to use the capabilities of their account. |
|---|---|
| Users | Customers. |
| Authorization-determination owner | Authorizations are requested by Product Managers according to customer needs and validated by the RSSI. |
| Access manager | IS administrators. |
| Access type | Reading 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 role | Enable DOMINO SAS employees to manage internal processes. |
|---|---|
| Users | DOMINO SAS employees. |
| Authorization-determination owner | Authorizations are discussed during specification workshops between the various stakeholders and validated by the RSSI. |
| Access manager | IS administrators for granting rights to groups; Managers for inviting employees into groups. |
| Access type | Reading 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 role | Machines on which processes execute. |
|---|---|
| Users | NA |
| Authorization-determination owner | IS administrators. |
| Access manager | IS administrators. |
| Access type | Compartmentalized 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 role | Partner servers (Mastercard, Arkéa and Exceet). |
|---|---|
| Users | NA |
| Authorization-determination owner | NA |
| Access manager | IS administrators. |
| Access type | Limited 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:
-
Connection to customer applications and their security is described in the chapter “Customer device authentication procedure”;
-
Connection to the Back Office for DOMINO SAS employees goes through a secure VPN that establishes a secure tunnel for all communications. Multi-factor authentication is required to access the VPN;
-
All exchanges between APIs and applications transit via HTTPS and are secured in TLS 1.2.
API / partner connection:
-
Mastercard / processing: as part of its CloudEdge offering, Mastercard provides to DOMINO SAS (installs and maintains) a MIP (Message Integrity Protocol) server equipped with an HSM (Hardware Security Module) that manages the security of exchanges between the scheme network and DOMINO SAS servers. Messages are internal to the AWS region and transit on port 443 in mTLS;
-
Mastercard / issuing: via HTTPS on port 443 in TLS 1.2;
-
Arkéa: in TCP/IP (non-HTTP) on port 443 in mTLS for the EBICS platform, and via the CFT protocol (non-HTTP) for files (with TLS 1.2 encryption);
-
Exceet: 443 in mTLS.
Exposure of APIs and databases to the internet:
-
Production databases are not exposed to the internet and can be accessed only by DOMINO SAS servers from inside the VPC;
-
Only port 443 is open and the servers accept only encrypted communications in TLS 1.2;
-
DOMINO SAS servers are exposed to the internet behind a WAF provided by AWS, which provides protection in particular against:
-
SQL injections: AWS WAF filters SQL injection attempts to protect web application databases;
-
Cross-site scripting (XSS): it protects against XSS attacks that may compromise user data;
-
Cross-site request forgery (CSRF): the WAF helps block CSRF attacks that exploit user sessions;
-
Buffer overflow: protection against attacks exploiting programming errors to execute malicious code;
-
DDoS attacks: mitigation of certain forms of DDoS attacks by traffic filtering;
-
Abusive automation and scraping: blocking abusive bots and scrapers;
-
Custom security rules: ability to create custom rules to meet specific security needs;
-
Protection against known vulnerabilities: WAF configuration to protect against specific vulnerabilities, such as those identified by OWASP.
-
Separation of the sensitive payment data management module:
-
The sensitive payment data management module (card and payment-account data) returns full data only to a whitelist of other modules;
-
This list is defined internally by DOMINO SAS and validated by the RSSI. Any other module will receive only truncated data (for example, the last 4 digits of the payment card) or the card or IBAN identification token.
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:
-
Black Box: penetration tests from the perspective of an external attacker with a minimum level of information made available to the pentesters;
-
Grey Box: penetration tests from the perspective of a standard user with an intermediate level of information shared with the pentesters;
-
White Box: penetration tests from the perspective of an administrator with the maximum level of information provided to the pentesters.
The scope of these tests covers:
-
Infrastructure, systems and networks;
-
Web applications.
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:
-
Interruption in the transaction service (SEPA and cards) => reputation and churn risk, Mastercard fine risk;
-
Platform unavailability for customers => reputation and churn risk;
-
Customer-service disruption => reputation and churn risk;
-
Data theft => legal and financial risk, reputation and churn risk;
-
Hacking and use of privileges => legal and financial risk; reputation and churn risk;
-
Disorganization => risk to the sustainability of the structure.
Causes of disruptions (with the related resulting disruption numbers):
-
Partial or total unavailability of the DOMINO SAS information system => 1, 2;
-
Partial or total network outage => 1, 2;
-
Partial or total power outage => 3, 6;
-
Unavailability of DOMINO SAS premises (fire, natural disaster, hundred-year flood, etc.) => 3, 6;
-
Unavailability of infrastructure hosted at AWS => 1, 2, 3, 6;
-
Information compromise (cyberattack, IT denial of service, virus, etc.) => 1, 2, 3, 4, 5, 6;
-
Loss of key person => 6;
-
Other cases: general transport strike, pandemic, terrorist attacks => 3, 6.
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:
-
Key people are replaced;
-
Loss of premises, a local network, power outages and other local disruptions have no real consequence because the company is designed to be “remote-first”;
-
Data theft and hacking constitute a significant risk. A robust backup system, modern architecture and high-quality partners allow rapid remediation.
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:
-
Communication procedures:
- Ensure that key information can be communicated effectively during a crisis through internal and external emergency communication channels.
-
Incident response plans:
-
The quality of immediate response procedures in the event of different types of incidents (natural disasters, IT outages, etc.);
-
Team responsiveness and effectiveness.
-
-
Systems and data recovery:
-
Operation of disaster recovery procedures for critical IT systems;
-
Integrity of data backups and restoration procedures.
-
-
System redundancy and contingencies:
- Operation of redundant systems and workaround solutions in place.
-
Roles and responsibilities:
-
That all team members know their roles and responsibilities in the event of a crisis;
-
Staff training.
-
-
External suppliers and partners:
-
Continuity plans of key suppliers and partners;
-
Ability of third parties to maintain services in the event of disruption at DOMINO SAS.
-
-
Regulatory compliance:
- Compliance of the plan with applicable regulations and industry best practices.
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:
-
Database restoration:
-
Test the ability to restore databases quickly from backups;
-
Verify the integrity and completeness of restored data.
-
-
Continuity of business processes:
- Simulate disruption scenarios and test the ability to maintain or quickly resume key business processes (transaction processing, customer service, online account management, etc.).
-
Operation of applications and platforms:
-
Test the availability and correct operation of applications and platforms essential to banking activity;
-
Test the ability to redeploy a functional version after introduction of a logical malfunction.
-
-
Performance of IT and network infrastructure:
- Test IT and network infrastructure to ensure that it can handle traffic and requests in the event of loss of part of the server instances.
-
Authentication and access security:
- Test authentication and access-control mechanisms after a restoration or failover.
-
Consistency of the elements described in the PUPA with one another;
-
Internal and external communication:
- Test internal and customer communication systems to ensure that they remain operational.
-
Interactions with partners and suppliers:
- Verify that connections and integrations with partner and supplier systems operate without interruption.
-
Transaction processing capacity:
- Test the infrastructure’s ability to process financial transactions without delay or error.
The test report contains the following information:
-
Summary of tests performed:
-
Description of simulated test scenarios;
-
List of procedures and systems tested (data restoration, business-process continuity, application operation, etc.).
-
-
Test objectives:
- Details of the specific objectives of each test (for example, RTO and RPO for data restoration).
-
Test results:
-
Detailed results for each test, including recovery times and success or failure in achieving objectives;
-
Problems and anomalies observed during tests.
-
-
Performance analysis:
-
Assessment of system and process performance during tests;
-
Comparison with expected performance objectives.
-
-
Incident and crisis management:
- Assessment of the effectiveness of incident and crisis management procedures.
-
Communication:
- Analysis of the effectiveness of internal and external communication channels during tests.
-
Integrations and partnerships:
- Assessment of the operation of integrations with partner and supplier systems.
-
Strengths and weaknesses:
- Identification of strengths and areas requiring improvement.
-
Recommendations:
-
Improvement suggestions based on test results;
-
Action plan to address identified weaknesses and problems.
-
-
Corrective-action planning:
- Schedule and responsibilities for implementation of corrective actions.
-
Signatures and approvals:
- Signatures of the persons responsible for conducting the tests and necessary approvals.
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:
- End customers access DOMINO SAS through customer applications, then through AWS WAF, into the production resource plane / VPC (Virtual Private Cloud).
- Within the production VPC, internal modules include a public API, business logic, integrations and a private API. Business logic exchanges with managed databases, which are backed up. Integrations exchange with providers.
- Green-Got employees access the Back Office through the VPN and then the private API.
- The development team accesses the Internal Developer Platform (IDP) through the VPN. The IDP connects to the VPC technical/security section, which contains monitoring/logging/alerting, deployment/artifact registry/platform orchestration, KMS and a bastion.
- The diagram highlights three focus areas: 1 - Internal Developer Platform, 2 - technical and security sections of the VPC, and 3 - internal modules.
1 - focus - Internal Developer Platform
Converted visual summary of image41.png:
-
The Internal Developer Platform (IDP) contains a developer control plane with:
-
IDE (Integrated Development Environment): code editors Visual Studio Code and JetBrains Rust, and Docker https://www.docker.com/;
-
VCS (Version Control Software): Git, GitHub https://github.com/, and IaC (Infrastructure as Code).
-
-
The IDP also contains an integration and delivery plane:
- CI (Continuous Integration) pipeline: GitHub Actions https://github.com/features/actions.
-
The development team accesses these components over TCP 443.
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:
-
VPN: allows unique user identification for role-based permissions in developer-facing services. Each session is registered and monitored. Developer team access is shown on TCP 443.
-
Monitoring, logging and alerting stack:
-
Prometheus (open source) collects time-series data from services, stores them in a time-series database and provides query capabilities. It includes alerting components that trigger alerts based on thresholds or conditions. Alerts are sent to email, Slack and SMS.
-
Grafana (open source) displays dashboards and visualizations for historical and real-time data. Grafana also displays and manages alerts.
-
The data ingestion and visualization service connects to local databases over TCP 5432. Other services send activity logs to Prometheus over TCP 80.
-
-
Deployer / artifacts registry / platform orchestrator:
-
In-house build service used to list finished builds and their documentation, start builds and deploy to environments.
-
Argo CD processes deployments: https://argoproj.github.io/cd/.
-
The web interface connects to a local database over TCP 5432 and other services over TCP 443 to deploy and provide secrets.
-
-
KMS (key management system):
-
Manages all keys for all environments, based on Hashicorp Vault or a Scaleway in-house product.
-
The web interface connects to a local database over TCP 5432 and other services over TCP 443.
-
-
Bastion:
-
Jump host to access internal resources through SSH.
-
Isolated from the rest of the network and manages its own logging, monitoring and alerting stack.
-
System administrators access the bastion over TCP 22; all other services can be accessed through SSH from the bastion only.
-
These building blocks allow DOMINO SAS teams to manage:
-
Access rights to the IS;
-
Keys;
-
Monitoring;
-
Alerting;
-
Deployment.
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:
-
Internal modules are described as one simple executable holding all logic and integrations.
-
Integrations are abstractions built over data stores to call providers and respond to their queries.
-
Public APIs include:
-
An authentication layer with an authentication module;
-
Commands, including a Retail API module and a Business API module.
-
-
Business logic is represented as multiple internal modules.
-
The private API includes:
-
Commands with an authentication service;
-
An internal back-office web service allowing Green-Got teams to manage internal processes.
-
-
Store abstractions are CRUD abstractions built over data stores to manipulate data.
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:
-
A load balancer / WAF fronts an AWS region.
-
The AWS region contains three availability zones. Each availability zone contains replicated container instances.
-
A separate managed data plane contains databases labeled Database 1, Database 2 and Database 3.
-
The application containers connect to the managed data plane.
-
Each “container instance” embeds all modules (simplified diagram for illustrative purposes - greater number than represented);
-
Each instance is a web server that exposes services on different internal ports;
-
The load balancer maps domains to ports;
-
The service called in the called instance then processes the request;
-
Each instance exposes the same services in the same way; this is full redundancy of the entire IS;
-
All instances call the same databases. To avoid double requests and race conditions, DOMINO SAS uses an event-based system for all writes involving sensitive payment data in order to ensure their processing synchronously;
-
The databases are fully managed and live in the same AWS region as the containers.
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 customers access the IS via the mobile application or the website. Access to these interfaces is through DOMINO SAS identifiers;
-
Employees working remotely access the IS via a VPN platform.
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:
-
There is no physical internal access other than that held by the host (AWS);
-
DOMINO SAS treats API access to IS resources via a VPN as external access. The procedures are the same as for DOMINO SAS employees connecting to the rest of the IS;
-
Administrator access is limited to the three administrators;
-
Access to the audit trail is limited to one person;
-
Access to resources (via the AWS interface) is subject to two-factor login and is limited by rights assigned by administrators through the “AWS Control Tower” cloud access governance service. Access to the root account is limited to the CTO only.
Converted visual summary of image18.png:
- IAM Identity Center, AWS Organizations and AWS Service Catalog feed into AWS Control Tower.
- AWS Control Tower creates, orchestrates and monitors the multi-account environment.
- Administrators set up the landing zone, manage access and distribution, and manage account members.
- The landing zone setup is automated based on best practices.
- Controls are enabled across the environment at scale to meet security and compliance requirements.
- Accounts are created, enrolled and updated in an account factory through an automated account provisioning workflow.
- The landing zone is monitored, including organizational units (OUs), accounts and controls.
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:
-
Recette: development environment;
-
Pre-Production: environment used to ensure upstream compliance and correct operation of services before production deployment.
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:
-
Access control;
-
Camera surveillance;
-
Security alarms;
-
Fire protection;
-
Safety and emergency lighting;
-
Evacuation and emergency plan;
-
Visitor management;
-
Secure internet network.
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
-
The user goes to the application;
-
The user starts onboarding by clicking the create-account button;
-
Onboarding data will be sent progressively to DOMINO SAS servers over HTTPS (TLS 1.2);
-
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;
-
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;
-
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);
-
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);
-
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;
-
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;
-
Asynchronously, Stripe sends a payment notification to a webhook opened on the DOMINO SAS servers dedicated to this partner (over HTTPS, on port 443);
-
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);
-
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);
-
This step depends on the setup; see sections 11.3.1.1.2 and 11.3.1.1.3 below;
-
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
-
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;
-
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;
-
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
-
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;
-
The received top-up is credited to the customer’s account;
-
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;
-
Mastercard is notified of the card creation.
Converted visual summary of image61.png:
-
Client device to DOMINO SAS VPC:
-
The client device communicates with DOMINO SAS VPC through AWS WAF over port 443 using TLS.
-
The DOMINO SAS VPC contains managed databases and business logic modules.
-
-
External onboarding providers:
-
The client device interacts with ubble.ai for identity verification and Stripe for initial funding.
-
ubble.ai and Stripe send partner callbacks/webhooks back through AWS WAF to the DOMINO SAS VPC over port 443 using TLS.
-
-
Card and notification partners:
-
DOMINO SAS VPC communicates with Exceet over port 443 using mTLS.
-
DOMINO SAS VPC communicates with Mastercard over port 443 using mTLS.
-
DOMINO SAS VPC communicates with customer.io over port 443 using TLS.
-
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:
- The client device sends the beneficiary request through AWS WAF to the DOMINO SAS VPC over port 443 using TLS.
- The DOMINO SAS VPC contains managed databases and business logic modules that process the request.
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:
- The client device sends the SCT request through AWS WAF to the DOMINO SAS VPC over port 443 using TLS.
- The DOMINO SAS VPC contains managed databases, business logic modules, an EBICS module and a CFT module.
- The DOMINO SAS EBICS module connects to Arkéa’s EBICS module over port 443 using TLS, signed and certified.
- The DOMINO SAS CFT module connects to Arkéa’s CFT module over SFTP with certificate.
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:
-
Mastercard servers connect to the Mastercard MIP / HSM (CloudEdge) located in the AWS region. The protocol label between Mastercard servers and the MIP / HSM is shown as
??in the source visual. -
Mastercard MIP / HSM connects to the DOMINO SAS VPC over port 443 using mTLS.
-
The DOMINO SAS VPC contains managed databases, business logic modules, an EBICS module and a CFT module.
-
DOMINO SAS responds to Mastercard servers over port 443 using mTLS.
-
For Arkéa:
-
The DOMINO SAS EBICS module connects to Arkéa’s EBICS module over port 443 using TLS, signed and certified.
-
The DOMINO SAS CFT module connects to Arkéa’s CFT module over SFTP with certificate.
-
Making an ATM withdrawal
An ATM withdrawal works in the same way as a card payment from the network and security standpoint:
-
An authorization to be given to Mastercard;
-
Transfer orders to be given to Arkéa.
Technical characteristics of payment instruments
-
SEPA:
-
DOMINO SAS processes SDD B2B direct debits, SDD Core direct debits and outgoing SEPA transfers;
-
DOMINO SAS accepts incoming SEPA transfers.
-
-
Payment card: DOMINO SAS offers Mastercard payment cards, with chip and magnetic stripe, issued by DOMINO SAS. All cards offered by DOMINO SAS have a contactless option (up to 50€). The forecast card offering included in the business plan will be as follows:
-
Standard retail customer: Mastercard Retail Standard;
-
Premium retail customer: Mastercard Retail Gold;
-
Micro-enterprise & standard business: Mastercard Business Standard;
-
Micro-enterprise & premium business: Mastercard Business Gold.
-
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:
-
The customer requests authentication on the mobile application;
-
The authentication service verifies that the customer exists in the database;
-
The authentication server sends an authentication code to the customer’s email address;
-
The end customer reads the OTP (One Time Password) in the email;
-
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;
-
The authentication servers store the session token and signature. This step triggers deletion of any previous session for this user;
-
The data are propagated for replication in all replicas;
-
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;
-
An email notification is sent to the customer. If the customer did not initiate this login, the customer must contact support urgently.
Operating phase:
-
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;
-
The authentication server retrieves the user and compares the session token and the expected HMAC;
-
The authentication server returns a 401 if authentication fails;
-
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:
-
Improved user experience: no password to remember means no possibility of forgetting it;
-
Credential-based attacks:
-
Password-based attacks such as brute force, dictionary attacks and credential stuffing are practically impossible;
-
This also eliminates risks associated with weak, reused or shared passwords.
-
-
No passwords means no password leaks: no possibility of password leaks on the DOMINO SAS side;
-
Protection against replay attacks: replay attack impossible because each request must be signed and timestamped and is unique;
-
Session hijacking: impossible for the same reason;
-
Credential protection: since the signature is never exchanged after initiation, the only way to steal it is direct access to the secure storage of the mobile phone;
-
Reduced password-management costs: password reset process, password policy and support calls related to forgotten passwords are not required given the absence of passwords. This implies cost reduction and process simplification for compliance;
-
Reduced phishing risks: users are less likely to suffer phishing attacks when there is no password to steal;
-
Easy revocation:
-
Token-based authentication, including OAuth, has revocation mechanisms that are error-prone and often require a token blacklist;
-
Checking a blacklist means overhead, and managing cache invalidation for the blacklist is an error-prone problem;
-
Since the session lives in memory, revocation is easy and instant.
-
-
Speed: it is a simple memory access;
-
Decoupling of user rights from authentication: the reason for many misconfigurations.
Converted visual summary of image74.png:
-
Signature-based passwordless authentication, initiation:
-
Mobile user requests authentication from the authentication service.
-
The authentication service checks that the customer exists in the authentication database.
-
The authentication service asks the customer email provider to send an authentication code.
-
The mobile user reads the OTP (One Time Password) from email.
-
The mobile user returns the OTP, the last 4 digits of the physical card and a self-generated signature; the mobile application stores the signature.
-
The authentication service sends the session token to the mobile user, stores the session token and signature in the authentication database, and deletes any previous session for this user.
-
The authentication database replicates the data to authentication service replicas.
-
-
Signature-based passwordless authentication, normal operation:
-
The mobile user makes a request with a session token and an HMAC generated with the stored signature.
-
The authentication service retrieves the user and compares the session token and expected HMAC against the authentication database.
-
If authentication succeeds, the authentication service forwards the request to internal services (Service 1, Service 2, …, Service N).
-
If authentication fails, the authentication service returns 401.
-
-
Recovery/contact flow:
- The mobile user contacts the customer-care team by email or chat application.
List of main formalized procedures relating to IT systems
The main formalized procedures relating to DOMINO SAS information systems are as follows:
-
Emergency and Business Continuity Plan (PUPA);
-
IS Security Policy;
-
Incident Management Procedure;
-
Complaints Management Procedure;
-
Fraud Management Procedure.
These procedures are included as annexes.
Project implementation schedule
Projected schedule:
Converted visual summary of image28.png:
| Date / period | Milestone |
|---|---|
| Oct. 2023 | Meeting to present the project to the ACPR |
| Jan. 2024 | Filing of the dossier |
| June 2024 | Authorization subject to conditions precedent (Agrément sous conditions suspensives) |
| Sept. 2024 | Final authorization (Agrément définitif) |
| Q4 2024 | CBS operational and integrated with partners |
| Q1 2025 | Restricted B2B and B2C “Friends and Family” launch under Green-Got authorization |
| End Q1 2025 | Commercial B2B launch under Green-Got authorization |
| Q2 2025 | B2C launch under Green-Got authorization |
| Q3 2025 | B2C 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
-
Step 1: Pre-authorization period, operating as PPS EU agent for all customers
-
During this period, all DOMINO SAS customers (in France & Belgium, individuals and sole traders) are under EPS’s authorization and DOMINO SAS acts as EPS’s agent.
-
This period began when DOMINO SAS’s account offering was launched (in 2022) and will last until final authorization is obtained.
-
DOMINO SAS began technical developments and modification of its governance and organization before preparing the authorization dossier and will finalize them once authorization subject to conditions precedent is obtained.
-
The first tests are performed as soon as final authorization is obtained.
-
-
Step 2: Ramp-up period
-
End of Q1 2025: commercial launch and first onboarding of legal-person customers under DOMINO SAS’s authorization. They will have access to the same service (modulo BE IBAN); only the general terms of use will be different. Only legal persons registered in France will be accepted initially.
-
Q2 2025: onboarding of new natural-person customers under DOMINO SAS’s authorization. Customers already present under EPS’s authorization are not migrated to DOMINO SAS’s authorization at this stage.
-
-
Step 3: Migration and run
-
Beginning of Q3 2025: reduced switch-over: migration tests for natural persons are performed, first with employee accounts, then with Friends & Family accounts.
-
End of Q3 2025: after an intensive communication campaign by email, SMS, on the website, in the application (reminder during each transfer) and on social networks encouraging customers to modify their direct debits, alert counterparties likely to send them transfers, and reissuance of payment cards: risk-free EPS natural-person accounts with an FR IBAN are migrated to DOMINO SAS’s authorization.
-
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.
- BE customers keep their accounts within EPS until a substitute actor is identified.
3-year project schedule
Before obtaining authorization:
DOMINO SAS will finalize technical developments concerning:
-
Technical foundation for deployment of the internal IS;
-
Preparation of the processing module
-
ISO 8583 translation module;
-
Authorization module.
-
-
Preparation of the issuing module
-
Exceet integration;
-
Tokenization module.
-
-
Internal Core Banking: normalized ledger, accounts and balances, event ledger, IBAN management;
-
Preparation of Arkéa integration
-
ISO 20022 translation module;
-
Complete servers on the proposed API surface.
-
-
Internal cash management module (subscriptions, transfers, etc.);
-
Public API;
-
Private API
- Connection of the private API to the back office.
-
Module to support compliance procedures (including SAR);
-
Migration of legacy integrations to the new IS:
-
ubble;
-
efficiale;
-
Generali;
-
Hawk:AI;
-
Company directory;
-
Arkineo;
-
Intercom;
-
Yousign;
-
customer.io;
-
Ficoba.
-
-
User management module
-
Onboarding;
-
Natural-person management;
-
Legal-person management;
-
Migration of AML-CFT support.
-
-
Launch of business applications.
DOMINO SAS will sign the following contracts:
-
Periodic control: with the firm selected in the ongoing market consultation;
-
Credit Institution: with Arkéa;
-
Card Processing and principal member contract: with Mastercard;
-
Card issuing: with Exceet;
-
Statutory audit: with BDO.
Upon obtaining authorization:
-
Arkéa integration
-
EBICS platform;
-
CFT platform;
-
Testing/acceptance (recette).
-
-
Issuing module
- BIN management and PAN creation / assignment module.
-
Processing module
-
Mastercard CloudEdge integration;
-
Apata integration for ACS;
-
Re-validation of Apple Pay / Google Pay (MDES).
-
-
Migration of legacy elements
-
Life insurance;
-
Wallets;
-
Round-up;
-
Savings accounts;
-
Back office;
-
KYC;
-
KYB;
-
Retail application to be connected to the new system.
-
-
Construction of new functionalities
-
Quote management (pro);
-
Invoice management (pro);
-
Savings accounts with CFCAL.
-
-
Continuous improvement.
Source Footnotes
https://paleblue.vc/
https://www.ipsos.com/sites/default/files/ct/news/documents/2023-10/Ipsos-Cese-Etat-de-la-France-septembre-2023%201.pdf
https://www.ipsos.com/sites/default/files/ct/news/documents/2023-10/Ipsos-Cese-Etat-de-la-France-septembre-2023%201.pdf
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
https://www.economie.gouv.fr/facileco/principaux-chiffres-france-et-europe#
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.
https://www.statista.com/statistics/944142/banking-population-in-europe-by-country/
https://www.grandviewresearch.com/industry-analysis/neobanking-market
https://www.statista.com/statistics/971163/customers-of-selected-european-neobanks-and-challenger-banks/
https://www.reuters.com/business/finance/monzo-valuation-jumps-45-billion-after-fresh-funding-round-2021-12-08/
https://www.ctvc.co/climate-tech-h1-2023-venture-funding/
https://tech.eu/2023/06/29/europe-dominates-in-climate-fintech-funding-despite-overall-funding-slump-in-2022/
https://www.bankingonclimatechaos.org/
https://green-got.com/articles/selection-assurance-vie
https://www.lerevenu.com/placements/assurance-vie/lassurance-vie-concerne-surtout-les-seniors
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*.
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 category | Retention basis | Retention period |
|---|---|---|
| Customer relationship and KYC records | French AML/CFT obligations under Code monetaire et financier Article L561-12 | 5 years after account closure or end of the customer relationship |
| Customer operation and payment records | French AML/CFT obligations under Code monetaire et financier Article L561-12 | 5 years after execution of the operation |
| Closed card records and required cardholder data | Legal, regulatory, accounting, tax, dispute, audit, or compliance purpose applicable to the closed account or related operations | 5-10 years after account closure, depending on the applicable retention duty |
| Payment dispute support data | Payment service dispute period under Code monetaire et financier Article L133-24 | At least 13 months after debit where needed to handle unauthorized or incorrectly executed payment claims |
| Tax audit supporting documents | Livre des procedures fiscales Article L102 B | 6 years from the last operation recorded or from document creation, depending on the record |
| Accounting documents and supporting evidence | Code de commerce Article L123-22 | 10 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:
- AML/CFT customer relationship and operation records are retained for 5 years.
- Tax audit records are retained for 6 years where applicable.
- Accounting records and supporting evidence are retained for 10 years where applicable.
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
- Code monetaire et financier Article L561-12: customer relationship, due-diligence, and operation records retained for 5 years.
- Code monetaire et financier Article L133-24: payment users may report unauthorized or incorrectly executed payment operations up to 13 months after debit.
- Livre des procedures fiscales Article L102 B: tax audit documents retained for 6 years.
- Code de commerce Article L123-22: accounting documents and supporting documents retained for 10 years.
- CNIL guidance on data retention: personal data retention periods must be justified by the processing purpose and documented by the controller.
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 type | Malware protection decision | Current controls |
|---|---|---|
| Windows workstations | Anti-malware required | Microsoft Defender, configured and enforced through Primo MDM |
| macOS workstations | Anti-malware required | Apple XProtect, built into macOS and not user-disableable; related endpoint controls monitored through Fleet |
| Linux workstations | Excluded from the workstation malware scanner scope | Linux work machines remain covered by endpoint security controls, patching, and employee-workstation risk review |
| Amazon Linux EC2 hosts used for ECS | AWS-native malware protection enabled | EC2 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 containers | Covered through server-side AWS-native and deployment controls | Container 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:
- Vulnerability Management Procedure documents the sources used to identify new vulnerabilities, including GitHub Dependabot, Amazon Inspector, Vanta Vulnerabilities, and related patching workflows.
- Software Vulnerabilities documents container and infrastructure scanning, including Amazon Inspector coverage for container images and EC2 hosts.
- Risk Management Policy defines the process for preparing, conducting, communicating, and maintaining risk assessments.
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.
-
Human users access AWS via AWS Identity Center (SSO). Identity Center sessions are allowed to last up to a maximum duration of 90 days. Reference: https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html
-
Resources running on AWS use AWS IAM roles (for example, instance/task roles) to obtain temporary credentials. Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html
-
Services running outside of AWS use AWS STS to assume roles and obtain temporary credentials. STS session tokens are short-lived (up to 12 hours maximum depending on role configuration). Reference: https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html
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.



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:
- Generated
- Rotated
- Revoked/retired
- Controlled and protected This could include vendor documentation if cloud service provider or HSM is used for key management activities. If CHD is not stored and encryption is not used to protect CHD, use the ‘Deactivate’ button and include an explanation.
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.

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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-04-30 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual 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.

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.

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
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-23 | julian@green-got.com | Document PAN protection controls in application flows. |
| 1.1 | 2026-04-30 | julian@green-got.com | Add 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
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:
- All processing flows of account data, including authorization, capture, settlement, chargeback, and refunds.
- All distinct acceptance channels, including card-present, card-not-present, and ecommerce.
- All types of data receipt or transmission, including any involving hard copy/paper media.
- The flow of account data from the point where it enters the environment, to its final disposition.
- Where account data is transmitted and processed, where it is stored, and whether storage is short term or long term.
- The source of all account data received (for example, customers, third party, etc.), and any entities with which account data is shared.
- Date of last update, and names of people that made and approved the updates.
Cardholder Data Flow Diagram
Document Control
| Field | Value |
|---|---|
| Document Status | 🔄 In Progress (Architecture designed, implementation ongoing) |
| Current Version | 0.2 |
| Last Updated | 2026-04-21 |
| Document Owner | Core Banking Team |
| Approved By | [Pending - CISO/QSA Approval Required] |
| Next Review Date | 2026-07-02 |
Version History
| Version | Date | Author | Approver | Changes | Status |
|---|---|---|---|---|---|
| 0.2 | 2026-04-21 | Core Banking Team | Pending | Add 3DS AAV authorization flow and apata_correlation_id documentation | In Progress |
| 0.1 | 2026-04-02 | Core Banking Team | Pending | Initial draft - Architecture and design documentation | In 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
- 1_card_holder_environment.md - CDE overview, actors, data classification
- 2_cryptography_key_management.md - Key architecture, HSM usage
- 3_pin.md - PIN structure, storage, verification
- 4_pan.md - PAN lifecycle, generation, inventory scope, encryption, HMAC lookup, reuse controls
- 6. 3DS - Authentication for 3DS challenges
- 8_emv_transactions.md - EMV transaction flows
- 14. Raw Authorization Messages Storage - Storage of raw ISO 8583 messages in ClickHouse, encryption, access controls
- 15. Clearing Files Processing - Processing of clearing files received from Mastercard, including archival/storage in S3 buckets, encryption, and access controls
- 16. ClickHouse Synchronization - Synchronization of PostgreSQL data to ClickHouse, including encrypted SAD/CHD, access controls
- 17. Automatic Billing Updater (ABU) - Mastercard service for updating cardholder information for recurring payments
Compliance Documentation
- Network Diagram - Network architecture and segmentation
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:
- Significant changes are made to the CDE architecture (new systems, removed components, changed data flows)
- New acceptance channels for card payments are added or removed
- Third-party connections to the CDE change
- Network segmentation changes affect cardholder data flows
- At minimum, once every 12 months as part of the annual review required by PCI DSS 12.5.2, and at least once every six months when confirming PCI DSS scope for service-provider requirements under PCI DSS 12.5.2.1
Update Procedure
- The Core Banking Team identifies the change and its impact on cardholder data flows.
- The diagram in 3_cardholder_data_flow.md is updated to reflect the change.
- 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”
- The updated diagram is submitted to the CISO for approval.
- 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:
- Version — incremented with each update
- Date — date the update was made
- Author — name of the person who made the update
- “Approved by” — name of the person who approved the update
- Changes — description of what was modified
- Status — current state of the document (In Progress / In Review / Approved)
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:
- CDE architecture changes, including new systems, removed systems, changed ingress paths, changed egress paths, or changed internal connectivity
- Network security controls change, including AWS Security Groups, AWS WAF rules, AWS Shield configuration, load balancers, route tables, Tailscale Grants, or private connectivity
- Third-party connections to or from the CDE change
- Administrative access paths to CDE systems change
- The cardholder data flow diagram changes in a way that affects network boundaries, connectivity, or data movement
- The PCI DSS scope is reviewed at least once every six months under PCI DSS 12.5.2.1
- Green-Got starts providing banking-as-a-service, payment infrastructure, hosted CDE components, or cardholder-data processing services to another PCI-assessed entity
Update Procedure
- The Core Banking Team identifies whether a planned or completed change affects CDE network boundaries, connections, security controls, or administrative access paths.
- The Network diagram is updated to reflect the current environment.
- The update is checked against 3_cardholder_data_flow.md so that network paths and cardholder data flows remain consistent.
- The change is reviewed through the normal pull request process before being merged.
- 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
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:
- TLS 1.3: Required for public HTTPS traffic and configured on the primary edge and load-balanced ingress paths. AWS service and API communications also negotiate TLS 1.3 where supported.
- TLS 1.2: Still appears in the CloudFront origin configuration because AWS does not currently expose a TLS 1.3-only origin enforcement setting in the
OriginSslProtocolsAPI. Reference: AWS CloudFront OriginSslProtocols. - Mutual TLS (mTLS): Used for certificate-authenticated HTTP and TCP connections where both sides must present and validate certificates.
- HTTPS: Used for public application traffic and encrypted service endpoints.
- HTTP: Used for server to load balancer traffic on internal trusted paths.
- SSH over Tailscale/WireGuard: Reserved for tightly controlled administrative access and disabled by default in production.
- SFTP: Used for secure file exchange with external partners where Green-Got acts as an SFTP client and not as the operator of its own hosted SFTP service.
- OpenPGP/PGP: Used to encrypt partner payloads and file-based exchanges before transmission.
The following cipher suites and encryption algorithms are explicitly represented in the repository and used for these flows:
- TLS_AES_128_GCM_SHA256: Used for TLS 1.3 connections to AWS-managed services.
- ECDHE-RSA-AES128-GCM-SHA256: Used for TLS 1.2 connections in AWS-managed administrative and load-balanced traffic.
- ChaCha20-Poly1305: Used for application-level encryption of sensitive internal secrets and stored cardholder data.
- AES-256-GCM: Used for data-at-rest encryption. AES key management and encryption for stored data are handled by AWS KMS.
- TDES 3-key in CBC mode with PKCS7 padding: Used only where a payment or card-personalization partner requires TDES-based cryptography. TDES operations are handled by AWS Payment Cryptography.
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.
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:
-
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.
-
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.

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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-04-30 |
| Document Owner | Platform / Security |
| Review Frequency | Annual 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 provider | Cryptographic function in scope | Evidence to attach in Vanta | Current evidence status |
|---|---|---|---|
| AWS | AWS 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éa | Arké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. |
| Mastercard | Mastercard 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. |
| Exceet | Exceet 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. |
| Apata | Apata 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:
- Cryptography & Key Management for the Card Domain
- PCI service provider inventory
- Vendor due diligence
- Vendor risk assessments
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-21 | enrico@green-got.com | Document outsourced cryptographic function coverage and service-provider evidence. |
| 1.1 | 2026-04-30 | julian@green-got.com | Clarify 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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-05-01 |
| Document Owner | Enrico Schaaf (Senior Software Developer) |
| Review Frequency | Annually 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:
| Technology | Usage | Algorithm |
|---|---|---|
| AWS KMS (Envelope Encryption) | KEK for all data-at-rest encryption of CHD/SAD in PostgreSQL | AES-256 (KEK), ChaCha20-Poly1305 (data encryption) |
| ChaCha20-Poly1305 | Local authenticated encryption of PAN, PIN, CVC, cardholder metadata via KMS-generated data keys | AEAD (256-bit key, 96-bit nonce, 128-bit tag) |
| HMAC-SHA256 | PAN matching/lookup index without decryption | Application-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 Cryptography | Partner communication with Arkéa-provided keys (Exceet, Mastercard, Apata) | TDES (required by partner protocol), AES-256, HMAC-256 (3DS AAV) |
| TLS 1.3 | All 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 used | TLS 1.3 |
| x.509 Certificates | TLS certificates for public endpoints | Managed via AWS Certificate Manager |
For full cryptographic architecture details, see Cryptography & Key Management for the Card Domain.
3. Trigger Events
| Trigger | Example | Detection Source |
|---|---|---|
| CVE in cryptographic library | Vulnerability 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 key | Unauthorized KMS Decrypt call, leaked data key material | AWS CloudTrail alerts in ClickStack |
| Compromised PAN lookup HMAC secret | Unauthorized retrieval of PAN lookup HMAC Parameter Store values, unexpected dual-slot mismatch, failed rotation verification | AWS CloudTrail alerts for AWS Systems Manager Parameter Store, application rotation metrics, change review |
| Deprecated cipher suite | CVE deprecating ChaCha20-Poly1305 or TDES weakness escalation | GitHub Dependabot, CVE feeds |
| Certificate expiration or trust-chain failure | TLS certificate nearing expiry or invalid chain | AWS Certificate Manager monitoring, ClickStack alerts |
| Configuration drift | Unencrypted traffic detected, TLS downgrade | ClickStack alerting (ALB logs), SigNoz alerting (HTTP 5xx, connection errors) |
| KMS, Parameter Store, or Payment Cryptography service disruption | AWS KMS API errors, Parameter Store retrieval failures, elevated latency on cryptographic calls | ClickStack alerts (CloudTrail), SigNoz alerts (application errors), AWS Health Dashboard |
| Key rotation overdue | Scheduled rotation interval exceeded for KMS KEK, PAN lookup HMAC startup secrets, or Arkéa-provided keys | Manual 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.
| Step | Action | Owner |
|---|---|---|
| 1. Detection | Identify failure via ClickStack alerts, SigNoz alerts, GitHub Dependabot, CVE feeds, or manual review | Any engineer |
| 2. Containment | Isolate 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 detected | Any engineer |
| 3. Assessment | Evaluate 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. Communication | Notify full engineering team via Slack (#devops). If CHD exposure is confirmed: notify Arkéa, legal counsel, and Vanta compliance contact | Enrico Schaaf |
| 5. Key/Cipher Rotation | Rotate 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-driven | Engineer assigned |
| 6. Patch or Reconfiguration | Update 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 Pulumi | Engineer assigned |
| 7. Validation | Run build and tests, verify encryption/decryption round-trips in staging before production deploy. Confirm via ClickStack and SigNoz that no error spikes post-deploy | Engineer assigned + reviewer |
| 8. Documentation | Log the event in a GitHub issue. Record: timeline, root cause, affected systems, actions taken, and lessons learned | Enrico Schaaf |
| 9. Post-Mortem Review | Review as a team. Update cryptographic inventory if keys or algorithms changed. Update this plan if process gaps are identified | Full team |
5. Tools & Inputs
Vulnerability Management Tools
- GitHub Dependabot — automated alerts for known vulnerabilities in all project dependencies, including cryptographic libraries. For simple dependency updates, a daily automation detects the Dependabot alert and automatically opens a PR. Any engineer is able to review, approve, and merge that PR
- Custom CI
Dependency Scanwithcargo-deny— software composition analysis before merge on dependency-impacting pull requests. The check evaluates Rust dependencies against advisory, yanked, and unmaintained-package policy and blocks new unaccepted findings.
SIEM / Alerting
- ClickStack (HyperDX) — infrastructure and audit logs (AWS CloudTrail, ALB access logs, Aurora PostgreSQL logs) are collected into ClickHouse. Alerts are configured for privilege escalation, authentication failures, infrastructure changes, and database incidents. Alerts route to the #devops Slack channel via webhook
- SigNoz — all application logs, traces, and metrics are collected via OpenTelemetry. Alerts are configured for production errors, HTTP 5xx, and application-level anomalies
KMS / Secret Management / HSM Logs
- AWS CloudTrail — append-only audit log of all AWS KMS and AWS Payment Cryptography API calls (GenerateDataKey, Decrypt, Encrypt, ImportKey, etc.), stored in S3
- AWS Systems Manager Parameter Store — protected storage for the active and secondary PAN lookup HMAC secrets loaded by the application at startup
- AWS KMS — KEK management, data key generation, automatic annual key rotation
- AWS Payment Cryptography — Arkéa-provided TDES/AES keys for partner communication (Exceet, Mastercard, Apata)
Crypto Inventory Logs
Policy References
6. Reporting & Audit Trail Requirements
Every cryptographic failure event is documented in a GitHub issue with the following fields:
| Field | Description |
|---|---|
| Detection timestamp | Date/time the failure was first detected |
| Detection source | ClickStack alert, SigNoz alert, Dependabot alert, CVE feed, manual discovery |
| Affected systems | Which components: KMS keys, Payment Cryptography keys, cryptographic libraries, TLS config, application services |
| Data at risk | Which CHD/SAD was potentially exposed (PAN, PIN, CVC, cardholder metadata) |
| Root cause | CVE, configuration error, key compromise, library vulnerability |
| Risk rating | Critical / High / Medium / Low |
| Containment time | Time from detection to isolation of affected system |
| Remediation time | Time from detection to full fix deployed in production |
| Actions taken | Key rotation, dependency update, config change, re-encryption |
| Reviewed by | Enrico 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).
| Name | Role | Date |
|---|---|---|
| Enrico Schaaf | Owner + Approver | |
| Second Engineer | Peer Reviewer |
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-05 | enrico@green-got.com | Document the cryptographic failure response process for PCI DSS evidence. |
| 1.1 | 2026-04-30 | julian@green-got.com | Replace 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.2 | 2026-05-01 | julian@green-got.com | Clarify 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:
- Documentation of impact (change description)
- Change approval
- Testing to verify code security (attach code review evidence such as a technical scan, code review results, or completed ‘Code Review Worksheet - OWASP Top 10 Checklist’
- Documentation of backout procedures or rollback plan
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.

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.

Approval is an explicit reviewer action. The confirmation step identifies the requester and the action before the reviewer confirms the approval.

After approval, the request detail view records the approved status and the operator who approved the request.

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:
- Sensitive data is retained across each in-scope system according to the requirements defined in your Data Management / Retention policy.
- Sensitive data is kept for no longer than the retention periods defined in your data retention matrix.
- 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 type | Shared with | Reason for sharing |
|---|---|---|
| VPC private addresses (RFC 1918) | Mastercard | Network 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 type | Shared with | Reason for sharing |
|---|---|---|
| Egress IP range | Hawk | IP allowlisting on their endpoints |
| Egress IP range | Apata | IP allowlisting on their endpoints |
| Egress IP range | exceet | IP 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:
- VPC private addresses (RFC 1918) are non-routable on the public internet. They are meaningless outside the VPC and provide no advantage to an attacker without prior access to the private network. Sharing them with Mastercard is a standard requirement for network integration and poses no risk.
- Egress IP addresses are not internal addresses — they are public IP addresses assigned to our outbound internet traffic. Every service we connect to already observes these addresses as part of normal TCP/IP communication. They are inherently discoverable and not confidential. We intentionally publish them via a public DNS record (
egress.ips.green-got.co) so partners can automatically query and update their allowlists. This further demonstrates that these addresses are treated as public information by design.
Security posture
Our security posture does not depend on the secrecy of any IP address:
- All systems in our cardholder data environment (CDE) are protected by authentication, TLS encryption, firewall rules, WAF, and network segmentation.
- None of these controls rely on IP addresses being secret.
- An attacker with knowledge of our VPC ranges or egress IPs gains no ability to bypass any security control.
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.
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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-06-08 |
| Document Owner | Platform / Security |
| Review Frequency | Annual 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:
| Environment | Key class | AWS account | Region / scope | Key ARN or identifier | Alias / resource name | Purpose | Owner | Rotation / retention |
|---|---|---|---|---|---|---|---|---|
| Production | Storage KEK | 489754752373 | eu-central-1 primary | arn:aws:kms:eu-central-1:489754752373:key/mrk-54ffeefb6b7c4a78afb967dd95d3cf66 | alias/encryption-key | Wraps KMS data keys used for application-side ChaCha20-Poly1305 storage encryption | Platform / Security | AWS KMS automatic rotation enabled; 30-day deletion window |
| Production | Storage KEK replica | 489754752373 | eu-west-3 replica | arn:aws:kms:eu-west-3:489754752373:key/mrk-54ffeefb6b7c4a78afb967dd95d3cf66 | alias/encryption-key | Regional continuity for the storage KEK | Platform / Security | Follows multi-region KMS key lifecycle; 30-day deletion window |
| Non-production | Storage KEK | 215052876796 | eu-central-1 primary | arn:aws:kms:eu-central-1:215052876796:key/mrk-2ca98a2781e04294bd3c46ae5591f8aa | alias/encryption-key | Staging and local-development storage encryption test key | Platform / Security | AWS KMS automatic rotation enabled; 30-day deletion window |
| Non-production | Storage KEK replica | 215052876796 | eu-west-3 replica | arn:aws:kms:eu-west-3:215052876796:key/mrk-2ca98a2781e04294bd3c46ae5591f8aa | alias/encryption-key | Regional continuity for the non-production storage KEK | Platform / Security | Follows multi-region KMS key lifecycle; 30-day deletion window |
| Production and non-production | Database volume key | Per AWS account and region | eu-central-1, eu-west-3 | Attach AWS KMS inventory export value for KeyArn | dbKmsKey | Aurora/RDS database volume encryption | Platform / Security | AWS KMS automatic rotation enabled |
| Production and non-production | Route 53 DNSSEC signing key | Per AWS account | us-east-1 | Attach AWS KMS inventory export value for KeyArn | DNSSEC signing key | Public hosted-zone DNSSEC signing | Platform / Security | Route 53 DNSSEC lifecycle; 7-day deletion window |
AWS Systems Manager Parameter Store secrets used for PAN lookup HMAC operations:
| Environment | Secret class | AWS account | Scope | Parameter identifier | Purpose | Owner | Rotation / retention |
|---|---|---|---|---|---|---|---|
| Production | PAN lookup HMAC active secret | 489754752373 | Production application configuration | Attach Parameter Store metadata for the active production SecureString value | Active HMAC-SHA256 key used by the application for keyed PAN lookup value generation and verification | Platform / Security | 256-bit key, 12-month cryptoperiod from activation date, maximum 30-day rotation window for backfill, verification, and previous-key retirement |
| Production | PAN lookup HMAC secondary secret | 489754752373 | Production application configuration | Attach Parameter Store metadata for the secondary production SecureString value | Secondary HMAC-SHA256 key loaded during rotation for timestamp-based dual-slot backfill | Platform / Security | 256-bit key, staged only for the current rotation, must not remain active beyond the 30-day rotation window |
| Non-production | PAN lookup HMAC active secret | 215052876796 | Non-production application configuration | Attach Parameter Store metadata for the active non-production SecureString value | Active non-production HMAC-SHA256 key used by the application for keyed PAN lookup value generation and verification | Platform / Security | 256-bit key, separate from production, rotated under non-production change management |
| Non-production | PAN lookup HMAC secondary secret | 215052876796 | Non-production application configuration | Attach Parameter Store metadata for the secondary non-production SecureString value | Secondary non-production HMAC-SHA256 key loaded during rotation testing and validation | Platform / Security | 256-bit key, separate from production, used only for non-production rotation testing and validation |
AWS Payment Cryptography keys used for card payment operations:
| Key class | Inventory identifier | Purpose | Owner / source | Operational status source | Rotation / retention |
|---|---|---|---|---|---|
| ZMK | AWS Payment Cryptography key ARN after import | Wraps and unwraps Arkéa-provided TR-31 key blocks | Arkéa key authority; Green-Got imports into AWS Payment Cryptography | AWS Payment Cryptography export | Current production AES-256 ZMK retained through end-2030 per Arkéa lifecycle; replacement ceremony before expiry or earlier if required |
| IMK-AC | imkac_tr31_dki_f1 / imported key ARN | Application cryptogram validation and generation support | Arkéa; shared with Exceet for personalization | AWS Payment Cryptography export | 3-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-IDN | imkidn_tr31_dki_f1 / imported key ARN | ICC dynamic number support | Arkéa; shared with Exceet for personalization | AWS Payment Cryptography export | 3-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-SMC | imksmc_tr31_dki_f1 / imported key ARN | Secure messaging confidentiality for issuer scripts | Arkéa; shared with Exceet for personalization | AWS Payment Cryptography export | 3-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-SMI | imksmi_tr31_dki_f1 / imported key ARN | Secure messaging integrity for issuer scripts | Arkéa; shared with Exceet for personalization | AWS Payment Cryptography export | 3-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_APATA | apata_zmk_arn | Wraps the KAAV export into the TR-31 block transmitted to Apata; Green-Got does not transmit ZMK material to Apata | Arkéa key authority; distributed by Arkéa for Apata exchange; Green-Got imports into AWS Payment Cryptography | AWS Payment Cryptography export | Current production AES-256 ZMK material retained through end-2030 per Arkéa lifecycle; replacement ceremony before expiry or earlier if required |
| KAAV_APATA_EXPORT | apata_export_kaav_tr31 / apata_export_kaav_key_arn | 3D Secure AAV/CAVV validation key generated by Green-Got and exported under the Arkéa-distributed Apata ZMK for partner distribution | Green-Got; exported KAAV TR-31 block shared with Apata | AWS Payment Cryptography export | Per Arkéa / Apata 3DS schedule |
| KCVV | kcvv_tr31_idx1 / imported key ARN | CVV/CVC computation and validation | Arkéa; Green-Got only | AWS Payment Cryptography export | 3-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 |
| KPVV | kpvv_tr31_idx1 / imported key ARN | PVV computation for online PIN verification | Arkéa; Green-Got only | AWS Payment Cryptography export | 3-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_EXCEET | exceet_pek_tr31 / imported key ARN | PIN block protection for Exceet manufacturing and personalization | Arkéa; shared with Exceet | AWS Payment Cryptography export | 3-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_MASTERCARD | mastercard_pek_tr31 / mastercard_pek_key_arn | PIN block protection for Mastercard authorization-system PIN encryption and decryption flows | Arkéa; Green-Got only | AWS Payment Cryptography export | 3-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
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-21 | julian@green-got.com | Document in-scope cryptographic key inventory and ownership evidence. |
| 1.1 | 2026-04-30 | julian@green-got.com | Replace AWS KMS HMAC inventory references with AWS Systems Manager Parameter Store active and secondary PAN lookup HMAC secrets and add document control metadata. |
| 1.2 | 2026-05-01 | julian@green-got.com | Clarify PAN lookup HMAC rotation-window usage and timestamp-based dual-slot backfill. |
| 1.3 | 2026-05-11 | julian@green-got.com | Align 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.4 | 2026-06-04 | julian@green-got.com | Document Arkéa-approved ZMK retention and TR-31 issuing/payment key cryptoperiods. |
| 1.5 | 2026-06-08 | julian@green-got.com | Clarify 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:
- “The principle of Least privilege” — ensuring users and systems operate with the minimum access necessary
- “Separation of duties” — preventing a single individual from having unchecked access
- “Establish secure defaults” — ensuring access is denied by default
- “Fail securely” — ensuring that failures do not result in elevated access
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:
- “Establish secure defaults” — ensuring session handling defaults to secure configurations
- “The principle of defense in depth” — applying multiple layers of session security controls
- “Fail securely” — ensuring session validation failures result in denied access rather than elevated access
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
- AWS Security Groups enforce ingress and egress rules between the load balancer, application instances, and databases
- AWS WAF enforces Layer 7 filtering on HTTP and HTTPS traffic at the edge
- AWS Shield provides managed DDoS protection for internet-facing AWS services
- Tailscale Grants restrict administrative and internal access paths into the environment
- AWS VPC route tables provide routing between VPC components and regions and are not used as an access control mechanism
Configuration standard
The following standards are defined and implemented for network security controls:
- Public ingress is limited to approved entry points only
- HTTPS on port 443 is the only public application protocol exposed to external clients
- Administrative access to internal systems is not exposed to the public internet and is restricted through Tailscale
- Database access is restricted to approved internal components on the database port only
- East-west traffic inside the AWS environment is restricted through security group rules
- Network security control changes are made through infrastructure configuration updates and maintained as version-controlled changes
Approved network paths
The following network paths are approved for the production environment:
- Public internet to AWS CloudFront over HTTPS (port 443)
- Public internet to AWS Global Accelerator over TCP/UDP 443 for approved mutual TLS integrations
- AWS CloudFront and AWS Global Accelerator to the application load balancer (only these origins are permitted by the load balancer security group)
- Application load balancer to application instances on port 80 (HTTP, internal only within the VPC)
- Application instances to the PostgreSQL database on port 5432
- Approved internal users, administrators, and automation through Tailscale according to granted access policies
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:
| Service | Protocol | Port | Business justification |
|---|---|---|---|
| Public application traffic | HTTPS | 443 | Serves the customer-facing banking application |
| Mutual TLS integrations | TCP/UDP | 443 | Required for approved payment and partner integrations via AWS Global Accelerator |
| Internal load balancer to application | HTTP | 80 | Routes traffic from the load balancer to application instances within the VPC |
| Database access | TCP | 5432 | PostgreSQL database access from application instances for transaction and account data |
| Internal application communication | Per security group rules | Per security group rules | East-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
- AWS Security Groups act as the primary network-level filtering control for inbound and outbound traffic between AWS components
- AWS WAF adds application-layer request filtering and rate-based protections for web traffic
- AWS Shield provides automated DDoS protections for internet-exposed AWS services
- Tailscale Grants limit internal and administrative access to approved identities and paths
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:
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:
| Alert | Severity | Package | Summary | Remediation PR | Resolution Time |
|---|---|---|---|---|---|
| #28 | High | libcrux-sha3 | Incorrect output from SHAKE squeeze functions | PR #1051 | 2 days |
| #26 | High | aws-lc-sys | CRL Distribution Point Scope Check Logic Error | PR #999 | 2 days |
| #25 | High | aws-lc-sys | X.509 Name Constraints Bypass via Wildcard/Unicode CN | PR #999 | 2 days |
| #22 | High | lz4_flex | Decompression can leak uninitialized memory | PR #958 | 14 hours |
| #21 | High | quinn-proto | Unauthenticated remote DoS via QUIC parameter parsing | PR #925 | 12 hours |
| #20 | High | aws-lc-sys | PKCS7_verify Signature Validation Bypass | PR #891 | 5 days |
| #19 | High | aws-lc-sys | Timing Side-Channel in AES-CCM Tag Verification | PR #891 | 5 days |
| #18 | High | aws-lc-sys | PKCS7_verify Certificate Chain Validation Bypass | PR #891 | 5 days |
| #2 | High | libyml | yaml_string_extend is unsound and unmaintained | PR #732 | 14 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:
- AWS ECS — A fully managed container orchestration service. Authentication is entirely IAM-based (task roles and execution roles). There is no login interface or default credential. ECS IAM
- Docker containers — All container images are built from our own source code. They do not include vendor-supplied default accounts or passwords. Our software inventory is documented in Custom Developed Software Inventory.
- EC2 host instances — The underlying EC2 instances run Amazon Linux 2023. No SSH key pairs are attached to the instances and port 22 is not open in any security group, making SSH access architecturally impossible. Shell access is provided exclusively through AWS Systems Manager Session Manager, which authenticates via IAM. The Pulumi infrastructure-as-code configuration (
src/infrastructure/environment/setup_region.ts) enforces this: the Launch Template has nokeyName, and the instance security group only permits traffic from within VPC CIDR blocks. Manage users on Linux instances
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
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:
- PAN, CVC, expiry, PIN display, and PIN set/change values are handled only inside the CDE-hosted iframe.
- The iframe is served from a dedicated CDE-controlled origin.
- Browser same-origin isolation prevents parent-page JavaScript from reading iframe DOM fields or rendered values.
- The mobile application opens the same iframe integration in a WebView.
- The shell receives only non-sensitive state, masked metadata, and success or failure results.
- The iframe and backend endpoints serving it remain in Green-Got’s PCI DSS Level 1 scope.
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
| Operation | Data involved | Scope treatment |
|---|---|---|
| Card display | PAN, expiry date, CVC | Values render only inside the CDE-hosted iframe. |
| PIN display | PIN | Value renders only inside the CDE-hosted iframe after step-up authentication and cardholder authorization. |
| PIN set or change | New PIN value | Cardholder enters the value only inside the CDE-hosted iframe. |
| Masked card overview | Masked PAN, card status, non-sensitive card metadata | Shell 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 application opens the approved CDE-hosted iframe in a WebView.
- The mobile application does not implement native fields for PAN, CVC, PIN, or PIN change.
- The WebView loads only the approved CDE iframe origin for cardholder operations.
- The native application does not expose JavaScript bridges or debugging interfaces that read iframe DOM, network responses, local storage, session storage, clipboard values, or iframe messages containing sensitive data.
- The native application does not log, screenshot, record, replay, analyze, or send support telemetry containing iframe sensitive values.
- The backend performs cardholder authentication and card ownership authorization before the iframe exposes card display, PIN display, or PIN set/change operations.
- Production builds enforce TLS and do not permit debug proxying or user-installed trust overrides for the CDE iframe flow.
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:
| Area | Stripe expectation | Green-Got equivalent |
|---|---|---|
| Hosted frame boundary | Sensitive 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 endpoint | The 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 access | Issuing Elements ephemeral keys expire after 15 minutes. | Iframe session authorization is short-lived and bound to the authenticated cardholder session. |
| PIN display | Stripe requires two-factor authentication before pages using PIN display. | Green-Got applies step-up authentication before PIN display and PIN set/change. |
| Native apps | Stripe directs native apps to load the web integration in a WebView. | Green-Got mobile loads the CDE-hosted iframe in a WebView. |
| PIN set/change | Stripe 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 context | Stripe 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:
| Source | Relevance |
|---|---|
| Using Issuing Elements | Hosted iframe display model, ephemeral-key flow, 15-minute expiry, PIN-display 2FA requirement, and native WebView guidance. |
| PIN management | PIN display, PIN set/change, encrypted PIN submission, no returned PIN in API responses, and online/offline PIN behavior. |
| Virtual cards with Issuing | PCI-DSS treatment for virtual card display and service-provider expectations for user-facing card programs. |
| Integration security guide | Shared PCI responsibility, low-risk integrations, TLS, CSP, webhook signature verification, and Stripe PCI Level 1 status. |
| Security at Stripe | Stripe PCI Service Provider Level 1 certification and PCI validation support by integration method. |
Residual Controls
The shell exclusion depends on the following controls:
| Control | Current state |
|---|---|
| CDE-hosted frame boundary | PAN entry, card display, PIN display, and PIN set/change occur only inside the CDE-hosted iframe. |
| No native sensitive fields | Web and mobile shells do not implement fields for full PAN, CVC, PIN, track data, card display values, or PIN-change values. |
| No sensitive telemetry | Logs, analytics, crash reports, session replay, screenshots, and support tooling exclude iframe sensitive values. |
| Non-sensitive parent contract | Parent-shell endpoints receive iframe URLs, non-sensitive session references, masked metadata, and event results only. |
| Step-up authentication | PIN display and PIN set/change require step-up authentication before the iframe exposes the operation. |
| Dedicated iframe origin | The iframe uses a dedicated CDE origin restricted by CSP and frame controls. |
| Shell release controls | Web and mobile shells are covered by secure software lifecycle, code review, dependency management, and release controls. |
Related Green-Got PCI Evidence
| Evidence | Purpose |
|---|---|
| 2026 H1 PCI DSS Scoping Exercise | Records 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 stored | Documents the backend issuer CDE treatment for stored PAN. |
| Primary Account Number (PAN) is masked when displayed | Documents default masked PAN display and customer-requested card-detail reveal behavior. |
| Payment page tamper detection | Records 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
Incident response considers legal and regulatory needs
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:
| Area | Documented handling |
|---|---|
| Legal and executive review | Legal 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 reporting | Breaches are reported to customers, consumers, data subjects, and regulators without undue delay and in accordance with contractual commitments and applicable legislation. |
| External communications approval | Incident or breach information is not disclosed to external parties or unauthorized persons without approval from legal or executive management. |
| Regulatory cooperation | Green-Got cooperates with customers, Data Controllers, and regulators to fulfill incident and data breach obligations. |
| CNIL notification | The 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 notification | The plan defines criteria for major operational or security incident reporting to ACPR and the initial, interim, and final report timelines. |
| Law enforcement notification | The 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.


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:
- Identifying common indicators of insider threat activity.
- Reporting suspicious activities through the appropriate communication channels.
- Understanding the risks that insider threats pose to cardholder data and the CDE.
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:
- The control owner (name and job title)
- The PCI DSS requirement the test maps to
- The evidence and current status of the control
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:
- screenshot or copy of an alert email, dashboard notification, or system log (e.g., multiple failed login attempts, unexpected network traffic, or a malware detection).
- screenshot of your security tool’s settings confirming that alerts are turned on and actively monitoring for threats. (A firewall rule in Palo Alto, Cisco, or Fortinet that triggers an alert when suspicious traffic is detected.)
- report or system log showing historical alerts and actions taken (e.g., a log from an EDR tool like SentinelOne or a SIEM system like Splunk or Datadog).
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:
| Step | Control |
|---|---|
| Detection | GuardDuty analyzes AWS foundational data sources and Runtime Monitoring telemetry. |
| Finding generation | GuardDuty creates findings for suspicious or malicious activity. |
| Routing | Amazon EventBridge receives GuardDuty findings. |
| Notification | SNS delivers email alerts to the security team. |
| Review | The security team triages findings through the incident response process. |
Supporting evidence:
- Intrusion detection system installation documents the deployed GuardDuty configuration.
- R-2254 - System Operations - Covert CnC Malware Detection Configuration documents CnC-specific GuardDuty detection and alert routing.
- Operational alert dashboard documents alert configuration and notification channels used for operational monitoring.
Example GuardDuty findings view:

Operational alerting screenshots:

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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-06-08 |
| Document Owner | Platform / Security |
| Review Frequency | Annual 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 group | Inventory entries |
|---|---|
| Arkea instant-payment mTLS and signing | cert_pk, cert_pk_passphrase, instant_payment_mtls_certificate, instant_payment_key_id, instant_payment_client_id, instant_payment_ca_cert |
| Arkea EBICS certificates and keys | ebics_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 OpenPGP | pgp_public_key, exceet_pgp_public_key, exceet_pgp_our_public_key, exceet_pgp_our_private_key |
| Partner SFTP and host keys | sftp_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 mTLS | mastercard_mtls_server_ca_bundle, mastercard_mtls_client_private_key, mastercard_mtls_client_cert_chain |
| Payment cryptography | imkac_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 family | Inventory entries | Issuance cryptoperiod | Authorization / validation retention | Renewal process |
|---|---|---|---|---|
| Arkéa ZMK | AWS Payment Cryptography ZMK ARN and Arkéa-distributed ZMK ARNs, including apata_zmk_arn | Retained through the end of 2030 for the current production AES-256 ZMK material, per Arkéa approval | Used 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 keys | imkac_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_tr31 | 3 years from activation for new card issuance and personalization | Retained 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 replacement | Arké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 keys | apata_export_kaav_tr31, apata_export_kaav_key_arn | Per Arkéa and Apata 3DS replacement schedule | Previous key retained only for the agreed validation overlap | Green-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 family | Green-Got inventory entry | Current expiry basis | External renewal tracking |
|---|---|---|---|
| Mastercard card-personalization PKI for Exceet production certificates | Expiry date communicated by Arkéa and/or Exceet for the Mastercard PKI used during card personalization | Mastercard PKI extension effective through the end of 2035; Green-Got does not issue cards with an expiry date later than the applicable Mastercard PKI validity | Arké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 key | Region / scope | Purpose | Rotation / lifecycle |
|---|---|---|---|
alias/encryption-key primary key | eu-central-1 | Multi-region key-encryption key for envelope encryption of sensitive application data | AWS KMS automatic rotation enabled; 30-day deletion window |
alias/encryption-key replica key | eu-west-3 | Replica for regional continuity of the envelope encryption key | Follows multi-region KMS lifecycle; 30-day deletion window |
dbKmsKey | eu-central-1 | Aurora/RDS database volume encryption | AWS KMS automatic rotation enabled |
dbKmsKey | eu-west-3 | Aurora/RDS database volume encryption for the secondary production region | AWS KMS automatic rotation enabled |
| Route 53 DNSSEC signing key | us-east-1 | DNSSEC key-signing key for the public hosted zone | Route 53 DNSSEC and AWS KMS lifecycle; 7-day deletion window |
AWS certificates:
| Certificate | Region / scope | Purpose | Renewal |
|---|---|---|---|
| Regional ACM certificates | Production regions | Public TLS for regional HTTPS endpoints and load balancers | ACM DNS validation and managed renewal |
| CloudFront ACM certificate | us-east-1 | Public TLS for CloudFront | ACM DNS validation and managed renewal |
AWS Parameter Store:
| Parameter | Purpose |
|---|---|
Production PAN lookup HMAC active SecureString value | Active 256-bit PAN lookup HMAC-SHA256 key loaded by the application at startup; 12-month cryptoperiod |
Production PAN lookup HMAC secondary SecureString value | Secondary 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 value | Active 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 value | Secondary 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 above | Secret backing values for Arkea, Exceet, and Mastercard key and certificate entries |
tls_cert | Let’s Encrypt internal service TLS certificate chain |
tls_key | Let’s Encrypt internal service TLS private key |
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-20 | enrico@green-got.com | Document key and certificate inventory evidence for production assets. |
| 1.1 | 2026-04-30 | julian@green-got.com | Replace AWS KMS HMAC inventory references with active and secondary AWS Systems Manager Parameter Store PAN lookup HMAC secrets and add document control metadata. |
| 1.2 | 2026-05-01 | julian@green-got.com | Clarify PAN lookup HMAC secondary secret usage during timestamp-based dual-slot backfill. |
| 1.3 | 2026-05-11 | julian@green-got.com | Align 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.4 | 2026-06-04 | julian@green-got.com | Add payment cryptography key cryptoperiods and Mastercard card-personalization PKI expiry tracking. |
| 1.5 | 2026-06-08 | julian@green-got.com | Remove 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:
- At least as strong as the DEKs they protect, and
- 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:
- Key Type:
SYMMETRIC_DEFAULT(AES-256) - Strength: 256-bit symmetric encryption, which meets or exceeds the strength of the 256-bit ChaCha20-Poly1305 data-encrypting keys
- Configuration: Multi-region primary/replica with automatic key rotation enabled
- Provisioning: The KEK is provisioned via Pulumi as the
encryption-keywith keyUsageENCRYPT_DECRYPT
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)
- The primary KEK is stored in AWS KMS hardware security module (HSM) in Frankfurt (eu-central-1)
- A read-only replica is maintained in Paris (eu-west-3) for disaster recovery
- Green-Got does not have direct access to the KEK material — AWS KMS holds and protects it
- Key operations (
GenerateDataKey,Decrypt) are performed via AWS KMS API calls, which never expose the key material to the application
DEK Storage (Database - KMS-Encrypted Form)
The application uses envelope encryption with the following flow:
-
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
- Requests a KMS-generated 256-bit data key from AWS KMS via
-
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
-
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
- Physical Separation: KEK in AWS KMS HSM (inaccessible), DEK only in plaintext during active operations
- Logical Separation: DEKs stored encrypted; each decryption requires a separate AWS KMS call
- Access Control: Accessing encrypted data requires both:
- AWS IAM permissions to call
kms:GenerateDataKeyandkms:Decrypt - Access to the encrypted_data_key blob stored in the database
- AWS IAM permissions to call
- Compromise Isolation: A database compromise alone does not allow decryption of encrypted DEKs — decryption requires the KEK held in AWS KMS
Summary
Green-Got satisfies both PCI DSS requirements:
- KEK Strength: AES-256 KEK is at least as strong as the 256-bit ChaCha20-Poly1305 DEKs it protects
- 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:
- User identification
- Type of event
- Date and time
- Success or failure indication
- Origination of event
- Identity or name of affected data, system component, or resource.
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:
- All actions taken by any individual with root or administrative privileges
- Access to all audit trails
- Invalid logical access attempts
- Use of and changes to identification and authentication mechanisms (e.g. elevation of privileges and changes, additions, or deletions to any account with root or administrative privileges)
- Initialization, stopping, or pausing of the audit logs.
- Creation and deletion of system-level objects Note: Each log should have the following details:
- User identification
- Type of event
- Date and time
- Success or failure indication
- Origination of event
- Identity or name of affected data, system component, or resource.
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.

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:
-
Enable Heartbeat alerting: Send periodic “heartbeat” events in your SIEM or logging tool.
-
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.
-
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.
-
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:
- Logs are enabled for common third-party applications in the environment.
- Logs are active by default.
- Logs are available for review only by the owning customer.
- Log locations are clearly communicated to the owning customer.
- Log data and availability is consistent with requirement 10.
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:
- Individual access to CHD
- Logging of privilege escalation (e.g. the use of SUDO or creation of an admin account)
- UserID
- Date and timestamp
- Affected resource (data, system component, or resource)
- Success or failure
- Source of event (e.g. system ID or IP)
- Alerts generated when logging is paused or stopped
- Changes to time settings
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):
- Privilege escalation — Root account usage, AssumeRole/AssumeRoot, events on AWS
- Authentication failures — Console login failures, access denied spikes
- Infrastructure changes — IAM destructive changes, S3 bucket policy, changes, CloudFormation stack deletions
- Database incidents — PostgreSQL FATAL/ERROR events, deadlocks, connection storms, schema mismatches from application code
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
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.

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.

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:
- access to the private network through Tailscale with an individually authenticated user identity, and
- application authentication through a WebAuthn passkey.
@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.
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.
Step 2 — Password (first factor): The user enters their 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.
Step 4 — Access granted: After completing both authentication factors, the user reaches the 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:
- knowledge factor: user name and password, and
- possession or inherence factor: authenticator code, security key, or biometrics.
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:
- MFA for Identity Center directory users
- Configure MFA in IAM Identity Center
- Supported MFA types in IAM Identity Center
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:
| Component | Version | Vendor support status |
|---|---|---|
| AWS Aurora PostgreSQL | 17.5 | Supported through at least 2029, extended lifecycle support enabled |
| Amazon Linux 2023 (ECS hosts) | AL2023 | Supported through 2028 |
| Debian stable-slim (container base) | Trixie | Active security support from the Debian Security Team |
| TLS | 1.3 minimum (TLSv1.3_2025 CloudFront policy) | Current standard per IETF RFC 8446 |
| AWS ECS | Current generation | Fully managed by AWS |
| AWS CloudFront | Current generation | Fully managed by AWS |
| AWS WAF v2 | Current generation | Fully managed by AWS |
| AWS GuardDuty | Current generation | Fully managed by AWS |
| AWS KMS | Current generation | Fully managed by AWS |
| ClickHouse Cloud | Current generation | Fully 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:
-
Daily security alert remediation (
fix_github_security_alertsautomation) — runs every day, queries open GitHub Dependabot alerts, and creates PRs to resolve security vulnerabilities. -
Weekday dependency updates (
update_dependenciesautomation) — 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:
- Windows workstations: Microsoft Defender — provides real-time threat detection, cloud-delivered protection, periodic scanning, and malware removal across all known malware types (viruses, Trojans, worms, spyware, ransomware, keyloggers, rootkits, and malicious scripts). Defender is configured and enforced via two dedicated Primo MDM profiles deployed to all Windows devices. Continuously monitored via Fleet osquery policies.
- macOS workstations: XProtect — Apple’s built-in anti-malware framework, providing signature-based malware detection and automatic remediation through XProtect Remediator. XProtect is integrated with Gatekeeper to block malicious software before execution. XProtect receives background updates from Apple independently of macOS system updates. Continuously monitored via Fleet osquery policies.
- Linux workstations — excluded from the workstation malware scanner scope through the platform assessment process. Linux work machines remain covered by endpoint security controls, patching, and employee-workstation risk review.
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 Control | OS | Status |
|---|---|---|
| Company default - Microsoft Defender Security Configuration | Windows 11 Pro | Deployed |
| Windows Devices - Microsoft Defender Maximum Protection | Windows 11 Pro | Deployed |

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 Policy | Purpose | Status |
|---|---|---|
| macOS - XProtect enabled | Verifies XProtect is present and active | Active |
| macOS - Firewall enabled | Verifies macOS firewall is on | Active |
| macOS - SIP enabled | Verifies System Integrity Protection is on | Active |
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:
- Windows: Microsoft Defender virus and threat definitions are updated automatically via Windows Update. Cloud-delivered protection and automatic sample submission are enabled.
- macOS: XProtect signature updates are delivered automatically by Apple multiple times per week in the background, independently of macOS system updates, ensuring definitions remain current without user intervention.
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:
- Windows: Microsoft Defender performs real-time (on-access) scanning of files upon open, close, rename, and download. Scheduled full-system scans run periodically as configured through Primo policy.
- macOS: XProtect performs signature-based scanning when applications are first launched, when they have changed, and when XProtect signatures are updated. XProtect Remediator runs periodic background scans to detect and remediate known malware.
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:
- Windows: Microsoft Defender logs all detection and remediation events in Windows Event Viewer (Defender/Operational). MDM policy enforcement is visible in the Primo MDM dashboard.
- macOS: XProtect logs detection and remediation events through the macOS Unified Logging system. Fleet policy audit trails are available in the Fleet dashboard under Policies > History.
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:
- Windows: Microsoft Defender Tamper Protection is enforced via Primo MDM. End users cannot disable Defender from the Windows Security app, Task Manager, or Services console. Only MDM-delivered policies via Primo can modify Defender configuration.
- macOS: XProtect is protected at the OS level by System Integrity Protection (SIP). XProtect cannot be disabled on macOS. SIP status is actively monitored via Fleet across all managed Macs.
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 type | Trigger | Destination | Tool |
|---|---|---|---|
| Policy failure | Any host fails a Fleet security policy | security@green-got.com | Fleet + Google Apps Script |
| Host offline | A device stops checking into Fleet | security@green-got.com | Fleet 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:
- malware-detection-workstations
- malware-detection-workstations-addigy
- malware-detection-workstations-jamf
- malware-detection-workstations-jumpcloud
- malware-detection-workstations-kandji
- malware-detection-workstations-microsoft-endpoint-manager
- malware-detection-workstations-rippling
- malware-detection-workstations-workspace-one
- received-macos-computer-malware-detection
- malware-detection-workstations-simplemdm
- malware-detection-workstations-manage-engine
- malware-detection-workstations-ninjaone
- malware-detection-workstations-datto
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:
- Windows workstations use Microsoft Defender, configured and enforced through Primo MDM.
- macOS workstations use Apple’s built-in XProtect framework, which is part of macOS and is not user-disableable. Fleet policies continuously verify related endpoint controls, including the macOS firewall and System Integrity Protection.
- Linux 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.
- Vanta’s automated Malware detection on personnel workstations (Primo) test continuously verifies antivirus presence across in-scope personnel workstations and is documented as passing with zero failing items in the submitted malware configuration evidence.
The submitted malware configuration evidence also documents:
- automatic anti-malware engine and definition updates for Microsoft Defender and XProtect;
- real-time and periodic scanning behavior on Windows and macOS;
- removable-media scanning behavior;
- audit logging and retention for anti-malware events;
- tamper protections preventing end users from disabling malware protections without administrative control.
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.
Monitor PCI industry trends
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:
| Area | Evidence |
|---|---|
| Technology and EOL monitoring | The 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 inventory | Maintenance 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 intelligence | Vulnerability Intelligence Sources documents active monitoring through GitHub Dependabot for known vulnerabilities in software dependencies. |
| Cryptographic protocols and cipher suites | Cipher 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 response | Cryptographic 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.


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.

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.

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:
- AWS VPC / Route Tables / Subnets — Managed via AWS Console/API with IAM. No login interface or default credentials exist. AWS VPC IAM
- AWS Security Groups — Managed via AWS Console/API with IAM. No login interface or default credentials exist. AWS VPC IAM
- AWS WAF / AWS Shield — Fully managed services with IAM-only access. No default credentials. WAF IAM
- AWS Application Load Balancer — Managed via IAM. No interactive login or default credentials. ELB IAM
- AWS CloudFront — Fully managed CDN with IAM-only access. No default credentials. CloudFront IAM
- AWS Global Accelerator — Fully managed with IAM-only access. No default credentials. Global Accelerator IAM
- Tailscale — Our mesh VPN. Tailscale is not an identity provider and does not use passwords. Authentication is delegated to an SSO identity provider. Tailscale SSO Providers
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:
- Boundary Definitions - Clear demarcation of network boundaries, such as firewalls, routers, gateways, and VPNs
- Network Devices - Servers, switches, bastion servers, routers, firewalls, and IDS/IPS systems with clear labels
- Subnets - Network subnets and their boundaries
- Data Flows - Representation of data flows in the network and the type of entity involved in the flow (e.g., public, private customer, internal employee, internal host, etc)
- Network Zones - Public, private, DMZ, or other specific zones like cardholder data environments for PCI DSS
- Security Controls - Firewalls, IDS/IPS systems, NAC systems, web application firewalls, etc.
- Critical Systems - Key databases, servers, and cryptographic servers
- Physical Locations - Network component locations (e.g., AWS US-East)
- Redundancy and Failover - Network load balancers and disaster recovery locations
Evidence
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:
- Connect the VPC to the internet for outgoing connections
- Connects the VPC from both regions
- Allows subnets to speak to each other
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
- 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.
- The application load balancer can only connect to our application on the provided port
- Our application can talk to the entire internet but can only be talked to from devices on our VPC.
- Only our application and our Network load balancer can talk to the database on the port of the database.
- The database does not have outgoing traffic.
- 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
-
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
-
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:
- A network topology diagram showing how production, development, and other environments are separated.
- Labels identifying firewalls, VLANs, subnets, DMZs, and restricted access controls.
- Screenshots of firewall rules, security groups, or network ACLs that enforce segregation between production and non-production environments.
- Screenshots or configurations showing Virtual Private Clouds (VPCs), Security Groups, and Network ACLs in AWS, Azure, or GCP.
- Subnet configurations demonstrating that production workloads are isolated within private subnets.
- Routing tables confirming restricted access between production and non-production environments.
- Evidence of Virtual Network Peering or AWS PrivateLink enforcing private, internal-only communication.
- Logs from SIEM, firewalls, or cloud security tools showing restricted traffic flow between production and other networks. A template can be found here: Google docs template
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:
- Minimum password length of at least 12 characters, or 8 characters if technically constrained (PCI 8.3.6)
- Passwords contain both alphabetic and numeric characters at a minimum (PCI 8.3.6)
- Password history of at least 4 previous passwords is enforced (PCI 8.3.7)
- Account lockout is triggered after no more than 10 failed attempts (PCI 8.3.4)
- Lockout duration is a minimum of 30 minutes or until the user’s identity is confirmed (PCI 8.3.4)
- Passwords change is enforced upon first use (PCI 8.3.5)
- Sessions are locked or require re-authentication after 15 minutes of inactivity (PCI 8.2.8)
- If passwords are used as the only authentication factor for user access, passwords/passphrases are changed at least once every 90 days OR the security posture of accounts is dynamically analyzed, and real-time access to resources is automatically determined accordingly. (PCI 8.3.9)
- If passwords are used as the only authentication factor for customer user access, passwords/passphrases are changed at least once every 90 days OR the security posture of accounts is dynamically analyzed, and real-time access to resources is automatically determined accordingly. (PCI 8.3.10.0)
- Strong cryptography is used to protect passwords in transit and at rest (PCI 8.3.2), including: – Use of HTTPS/TLS 1.2+ for login pages – Hashed/salted or encrypted storage of passwords in the database If you do not manage non-consumer user accounts, use the ‘Deactivate’ button in Vanta and include a written explanation.
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:
- Background checkers
- Cloud providers
- Communication platforms
- CRM platforms
- Database/Data warehouse providers
- Endpoint security tools
- HRIS
- Identity providers
- MDM tools
- Vulnerability scanners
- SIEM tools
- Version control systems
- DevOps tools
- 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


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

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
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
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
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
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:
- Every packet is individually authorized against network rules before it is transmitted and delivered (Logical Separation on AWS)
- Instances do not observe traffic that is not addressed to them; promiscuous mode does not reveal other instances’ traffic (EC2 Infrastructure Security)
- ARP spoofing is not possible on the AWS network, as ARP packets are not used for virtual network topology discovery
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

Example of an individual alert configuration

How the team is informed about alerts triggering

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 type | Critical criteria |
|---|---|
| Confirmed unauthorized access | Confirmed unauthorized access to production systems, administrative accounts, customer data, regulated data, or security tooling. |
| Active exploitation | Evidence of active exploitation, attacker persistence, command-and-control activity, malware execution, or unauthorized privileged activity. |
| Cardholder data exposure | Confirmed or suspected unauthorized access, disclosure, movement, storage, or transmission of cardholder data or sensitive authentication data. |
| Critical security control failure | Failure 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 impact | Outage, degradation, or data integrity issue affecting production service delivery, payment-card-related services, or incident response capabilities. |
| Insider threat or malicious internal activity | Evidence 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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-04-30 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual 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:
- AWS KMS holds the AES-256 key-encrypting key (KEK) in HSM-backed storage.
- A background data-key cache refreshes through AWS KMS, receiving a plaintext data key for in-memory encryption and an encrypted data key blob for storage.
- The application encrypts PAN locally with ChaCha20-Poly1305 using the KMS data key and a unique nonce.
- The persisted database value contains the KMS-encrypted data key and the ChaCha20-Poly1305 encrypted payload. The encrypted payload includes the nonce and authentication tag.
- The PAN and transient variables are zeroized after use.
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.
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-21 | julian@green-got.com | Document database PAN encryption and obfuscation controls. |
| 1.1 | 2026-04-30 | julian@green-got.com | Add 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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-04-30 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual 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:
- AWS KMS holds the AES-256 key-encrypting key (KEK) in HSM-backed storage. The KEK is not exported to Green-Got systems.
- A background data-key cache refreshes through AWS KMS
GenerateDataKey, receiving a plaintext data key and a KMS-encrypted data key blob. The application uses the plaintext data key only in process memory for active encryption work. - PAN is encrypted locally by the application with ChaCha20-Poly1305 using the KMS data key and a unique nonce for each encryption.
- The persisted database value is an envelope containing
[encrypted_key_length][encrypted_data_key][encrypted_payload]. The encrypted payload is produced by ChaCha20-Poly1305 and includes the nonce and authentication tag. - The PAN and transient variables are zeroized after use.
- Decryption requires both database access to the encrypted envelope and IAM-authorized access to AWS KMS for the encrypted data key.
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:?}"); }
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-21 | julian@green-got.com | Document PAN unreadable-at-rest controls and masked logging evidence. |
| 1.1 | 2026-04-30 | julian@green-got.com | Add 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:
- Customer-facing public traffic is exposed through HTTPS/TLS.
- CloudFront redirects HTTP to HTTPS and uses the
TLSv1.3_2025policy. - Mastercard file exchange uses SFTP/SSH transport with key-based authentication and host-key validation.
- Exceet card-manufacturing transfers use SFTP/SSH transport with host-key validation and an additional OpenPGP file-encryption layer.
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:
-
Head of Risk — Oversight over the execution of the information security and privacy risk management program and risk treatments. Responsible for maintaining compliance with relevant data privacy and information security laws and regulations. Responsible for adherence to company adopted information security and data privacy standards and frameworks. Measures compliance to policy through reports, internal/external audits, and feedback (jointly with CTO).
-
Chief Technology Officer (CTO) — Oversight over the implementation of information security controls for infrastructure and IT processes. Coordinates the development and maintenance of information security policies and standards. Oversight of implementation, operation, and monitoring of information security tools and processes in production environments. Measures compliance to policy (jointly with Head of Risk).
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:
- minimum password length of 14 characters,
- required uppercase characters,
- required lowercase characters,
- required numbers,
- required symbols,
- self-service password change enabled, and
- password reuse prevention set to 24 previous 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:
- Minimum password length of at least 12 characters, or 8 characters if technically constrained (PCI 8.3.6)
- Passwords contain both alphabetic and numeric characters at a minimum (PCI 8.3.6)
- Password history of at least 4 previous passwords is enforced (PCI 8.3.7)
- If passwords are used as the only authentication factor for user access, passwords/passphrases are changed at least once every 90 days OR the security posture of accounts is dynamically analyzed, and real-time access to resources is automatically determined accordingly. (PCI 8.3.9)
- Account lockout is triggered after no more than 10 failed attempts (PCI 8.3.4)
- Lockout duration is a minimum of 30 minutes or until the user’s identity is confirmed (PCI 8.3.4)
- Passwords change is enforced upon first use (PCI 8.3.5)
- Sessions are locked or require re-authentication after 15 minutes of inactivity (PCI 8.2.8)
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
- A method is implemented to confirm that each script that runs on payment pages is authorized.
- A method is implemented to assure the integrity of each script.
- An inventory of all approved scripts is maintained with written justification as to why each is necessary.
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
- Remediation Documentation: Submit records such as tickets, reports, or logs that detail the identified issues, assigned personnel, remediation steps, and current status.
- Follow-up Testing Reports: Results from subsequent tests confirming that previously identified vulnerabilities have been effectively addressed.
- Management Sign-off: Approvals from leadership acknowledging the completion and effectiveness of remediation efforts.
- Configuration Changes: Logs or records detailing modifications to system configurations to mitigate identified risks.
- Patch Management Records: Evidence of applied patches or updates to address vulnerabilities found during the penetration test.
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:
- The full CDE perimeter and all critical systems that store, process, transmit, or affect the security of cardholder data
- External internet-exposed entry points to trusted networks and critical systems
- Internal testing paths from inside trusted networks and toward the CDE
- Application-layer testing for the vulnerabilities listed in PCI DSS Requirement 6.2.4
- Network-layer testing covering systems that support network functions and operating systems
- Threats and vulnerabilities observed by Green-Got during the previous 12 months
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:
- The internal and external testing scope
- The tester and evidence of tester qualification and organizational independence
- The methodology used for application-layer and network-layer testing
- The tested systems, entry points, dates, source locations, tools, and manual procedures
- The security findings, severity ratings, affected systems, and exploitation evidence where applicable
- The assessment of threats and vulnerabilities observed by Green-Got during the previous 12 months
- The remediation and retest status for exploitable vulnerabilities and security weaknesses
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:
- Storage location is reviewed at least annually (this can be a ticket or email indicating that the location was reviewed)
- Media is inventoried, and authorized prior to being moved (provide screenshots, tickets, or records of media inventories and approvals such as signoffs for tape pickups and tracking details)
- If media containing CHD (hard drives, tapes, paper, etc.) was destroyed in the last year, provide a sample certificate of destruction from the vendor -or- ticket describing how media was securely destroyed
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:
- Entry and exit points to/from sensitive areas within the CDE are monitored.
- Monitoring devices or mechanisms are protected from tampering or disabling.
- Collected data is reviewed and correlated with other entries.
- Collected data is stored for at least three months, unless otherwise restricted by law.
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.
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:
- An up-to-date inventory of POS/POI devices is maintained
- A processes is being followed to periodically inspect devices for tampering, and employees are aware of the process (this is typically done via a task checklist or email acknowledgement)
- Sample of training materials for employees demonstrating POS/POI inspection and identity verification procedures for those claiming to be POS maintenance personnel have been created and distributed If no physical POS/POI devices are in use in your environment, use the Deactivate’ button and include an explanation.
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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-04-30 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual 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:
- Encrypted PAN: envelope-encrypted with AWS KMS-backed data keys and ChaCha20-Poly1305, as documented in pan/pan_rendered_unreadable.md.
- PAN keyed HMAC lookup value: keyed HMAC-SHA256 value used for deterministic matching without decrypting PAN.
- Masked PAN: display-only form, typically first six and last four digits, as documented in pan_masked/index.md.
- External correlation tokens: random or system-generated identifiers used with external providers, such as Apata, for request correlation. These tokens are not derived from PAN.
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:
- The encrypted PAN envelope requires database access and IAM-authorized AWS KMS decrypt permissions.
- The keyed HMAC lookup value is used internally for matching. HMAC operations require authorized access to the protected active and secondary PAN lookup HMAC secrets loaded from AWS Systems Manager Parameter Store at application startup, and the value is not displayed to operators or customers.
- External correlation tokens are not derived from PAN and are used instead of PAN-derived identifiers when correlating requests with third-party providers.
- The masked PAN is the default display value in application and support views.
- Clear PAN is exposed only for authorized flows that require the full card number, and it exists only transiently in application memory.
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-20 | julian@green-got.com | Document retained PAN representations and reconstruction-prevention controls. |
| 1.1 | 2026-04-30 | julian@green-got.com | Align 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.

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.

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


For Github we rely on Github’s native audit logs

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:
- Background checkers
- Cloud providers
- Communication platforms
- CRM platforms
- Database/Data warehouse providers
- Endpoint security tools
- HRIS
- Identity providers
- MDM tools
- Vulnerability scanners
- SIEM tools
- Version control systems
- DevOps tools
- 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:
- Employee Workstations – End-user devices like laptops/desktops that do not handle CHD/SAD directly.
- Internal Web Apps – Intranet portals or HR systems not involved in payment processing.
- Dev/Test Environments – Non-production systems where CHD/SAD is never present.
- Backup Systems (Non-CHD) – Infrastructure storing internal data backups unrelated to CHD/SAD.
- IT Admin Tools – Ticketing systems, asset management platforms, or remote tools not tied to CHD systems.
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:
- Provide screenshots of SIEM system demonstrating that relevant systems providing critical security controls are monitored continuously. Systems may include the following:
- Firewalls
- IDS/IPS
- File Integrity Monitoring
- Anti-virus
- Physical access controls
- Logical access controls
- Audit logging mechanisms
- Segmentation controls (if used)
- 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:
- Arkéa: arkea_cma_green-got_framework_agreement_2026-01-08.pdf. This agreement is the applicable evidence for the Mastercard-related business arrangement because Green-Got is not a Mastercard principal member and the arrangement is handled through Arkéa.
- Apata: apata_limited_software_licence_agreement.pdf.
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.

The ClickHouse Cloud backup configuration shows:
- Backup frequency: daily
- Last successful backup: 2 hours before the screenshot was captured
- Next scheduled backup: 22 hours after the screenshot was captured
- Recent backups completed successfully

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:
- Functions with differing security needs do not co-exist on the same system component
- Functions with differing security needs that exist on the same system component are isolated from each other
- Functions with differing security needs on the same system component are all secured to the level required by the function with the highest security need
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:
- Only trusted keys and certificates are accepted.
- Certificates used to safeguard PAN during transmission over open, public networks are confirmed as valid and are not expired or revoked. This bullet is a best practice until 31 March 2025
- The protocol in use supports only secure versions or configurations and does not support fallback to, or use of insecure versions, algorithms, key sizes, or implementations.
- The encryption strength is appropriate for the encryption methodology in use.
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:
- CloudFront redirects HTTP to HTTPS and uses the
TLSv1.3_2025policy for client-facing public traffic. - The Application Load Balancer uses the
ELBSecurityPolicy-TLS13-1-3-2021-06security policy. - CloudFront origin communication is configured as
https-only. - Mastercard file exchange uses SFTP/SSH transport with key-based authentication and host-key validation.
- Exceet card-manufacturing transfers use SFTP/SSH transport with host-key validation and an additional OpenPGP file-encryption layer.
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 type | Platform-specific scope decision | Documented controls and monitoring |
|---|---|---|
| Linux workstations | Linux 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 ECS | EC2 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 containers | Containers 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.
| Platform | Signature and engine update control | Evidence |
|---|---|---|
| Windows workstations | Microsoft 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 workstations | Apple 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 workstations | Linux 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 workloads | GuardDuty 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:

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:
| Platform | Current scan behavior | Supporting evidence |
|---|---|---|
| Windows workstations | Microsoft 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 workstations | XProtect 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 workstations | Linux 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 workloads | GuardDuty 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:

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:
- Code reviews ensure code is developed according to secure coding guidelines.
- Code reviews look for both existing and emerging software vulnerabilities.
- Appropriate corrections are implemented prior to release.
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:
- Reviewed by individuals other than the originating code author, and who are knowledgeable about code-review techniques and secure coding practices.
- Reviewed and approved by management prior to release.
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:
- Reason for, and description of, the change.
- Documentation of security impact.
- Documented change approval by authorized parties.
- Testing to verify that the change does not adversely impact system security.
- For bespoke and custom software changes, all updates are tested for compliance with Requirement 6.2.4 before being deployed into production.
- Procedures to address failures and return to a secure state.
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:
- Job classification and function
- Least privileges necessary to perform job function
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:
- Access is through applications or other programmatic methods, which access and allowed actions based on user roles and least privileges.
- Only the responsible administrator(s) can directly access or query repositories of stored cardholder data.
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:
- passwords are changed every 90 days OR
- security posture of accounts are dynamically analyzed, and real-time access to resources is automatically determined accordingly
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.


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 capability | Vendor documentation | CnC relevance |
|---|---|---|
| EC2 C&C network activity detection | GuardDuty EC2 finding types | Includes findings for EC2 instances querying IP addresses or domain names associated with known command-and-control infrastructure. |
| Runtime C&C activity detection | GuardDuty Runtime Monitoring finding types | Includes runtime findings for EC2 instances and containers querying domains associated with known command-and-control servers. |
| DNS-based data exfiltration detection | GuardDuty EC2 finding types | Includes DNS data exfiltration finding types, which cover malware or attacker behavior that uses DNS queries as an outbound channel. |
| Malware scan findings | Malware Protection for EC2 finding types | Documents GuardDuty Malware Protection for EC2 findings generated when suspicious or malicious files are detected on EC2-backed workloads. |
| GuardDuty-initiated malware scans | Findings that invoke GuardDuty-initiated malware scan | Documents 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 area | Current state |
|---|---|
| GuardDuty service | Enabled across the relevant AWS regions for the PCI environment. |
| Foundational data sources | GuardDuty consumes VPC Flow Log data, DNS query logs, and AWS CloudTrail events. |
| Runtime visibility | GuardDuty Runtime Monitoring is enabled for EC2 instances hosting ECS workloads, with the GuardDuty agent managed through AWS Systems Manager. |
| Malware scanning | GuardDuty Malware Protection for EC2 is enabled for EC2 and ECS-on-EC2 workloads. |
| Alert routing | GuardDuty 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:

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):
- Identifying all data flows for the various payment stages (for example, authorization, capture settlement, chargebacks, and refunds) and acceptance channels (for example, card-present, card-not-present, and e-commerce)
- Updating all data-flow diagrams per Requirement 1.2.4
- Identifying all locations where account data is stored, processed, and transmitted, including but not limited to: 1) any locations outside of the currently defined CDE, 2) applications that process CHD, 3) transmissions between systems and networks, and 4) file backups
- Identifying all system components in the CDE, connected to the CDE, or that could impact security of the CDE
- Identifying all segmentation controls in use and the environment(s) from which the CDE is segmented, including justification for environments being out of scope
- Identifying all connections from third-party entities with access to the CDE
- Confirming that all identified data flows, account data, system components, segmentation controls, and connections from third parties with access to the CDE are included in scope
- Showing exercise was performed within the last 12 months (12.5.2.a)
- If applicable, additional reports showing excercise is also performed after significant changes to the in-scope environment (12.5.2.a)
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:
- Identifying all data flows for the various payment stages (for example, authorization, capture settlement, chargebacks, and refunds) and acceptance channels (for example, card-present, card-not-present, and e-commerce).
- Updating all data-flow diagrams per Requirement 1.2.4.
- Identifying all locations where account data is stored, processed, and transmitted, including but not limited to: 1) any locations outside of the currently defined CDE, 2) applications that process CHD, 3) transmissions between systems and networks, and 4) file backups.
- Identifying all system components in the CDE, connected to the CDE, or that could impact security of the CDE.
- Identifying all segmentation controls in use and the environment(s) from which the CDE is segmented, including justification for environments being out of scope.
- Identifying all connections from third-party entities with access to the CDE.
- Confirming that all identified data flows, account data, system components, segmentation controls, and connections from third parties with access to the CDE are included in scope.
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.

Completed Reviews
| Period | Date | Conducted By | Document |
|---|---|---|---|
| 2026-H1 | 2026-04-05 | Enrico — Senior Software Engineer | Scoping Exercise 2026-H1 |
Scoping Exercise 2026-H1
PCI DSS Scope Review — 2026-H1
1. Review Metadata
| Field | Value |
|---|---|
| Date of Review | 2026-04-05 |
| Review Period | 2026-H1 (First semi-annual review) |
| Conducted By | Enrico — Senior Software Engineer |
| Applicable Requirements | 12.5.2, 12.5.2.1 (Service Provider) |
| Entity Classification | Service Provider — Level 1 |
| Review Trigger | Initial 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 Review | 2026-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:
- 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.
- 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.
- 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.
- 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.
- Segmentation control verification — All segmentation mechanisms separating the CDE from out-of-scope environments were reviewed for correctness and effectiveness.
- 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 Stage | Applicable | Description |
|---|---|---|
| Authorization | Yes | EMV 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. |
| Capture | Yes | Transaction capture data received from Mastercard network. |
| Settlement | Yes | Settlement processing via Arkéa as principal member. Includes SEPA and instant payment flows. |
| Chargebacks/Disputes | Yes | Chargeback and dispute processing via Mastercard and Arkéa. |
| Refunds | Yes | Refund processing via Mastercard and Arkéa. |
3.2 Data Flows — Acceptance Channels
| Acceptance Channel | Type | Description |
|---|---|---|
| EMV Chip Contact with PIN | Card-Present | Standard inserted-card transaction with online PIN verification. |
| Contactless without PIN | Card-Present | Tap-to-pay transactions below €50 threshold. Mastercard Europe limits: 5 consecutive transactions or €150 cumulative without PIN. |
| Contactless with PIN | Card-Present | Tap-to-pay transactions at or above €50 threshold, requiring PIN entry. |
| E-commerce (3D Secure) | Card-Not-Present | Online transactions authenticated via 3D Secure through Apata (AAV validation). |
| Mobile Wallet (MDES) | Card-Not-Present | Mastercard Digital Enablement Service tokenized transactions via mobile wallets. |
| ATM | Card-Present | Cash 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
| Location | Data Stored | Encryption | Retention | Purpose |
|---|---|---|---|---|
| Aurora Global Database (Postgres) — Primary: eu-central-1, Replica: eu-west-3 | PAN (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
| Location | Data Processed | Description |
|---|---|---|
| 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 KMS | Data encryption keys, KEK | Key wrapping/unwrapping for internal storage encryption. Does not see clear CHD. |
| AWS Payment Cryptography | PIN blocks, CVC, cryptograms, ARQC/ARPC | TDES/AES cryptographic operations for partner communication. Processes SAD for card network operations. |
3.6 Account Data — Transmission Paths
| From | To | Data Transmitted | Transport Security |
|---|---|---|---|
| Mastercard | Core Banking Service | Authorization requests, capture, settlement, chargebacks, refunds (PAN, SAD) | AWS Direct Connect; encrypted in transit |
| Core Banking Service | Mastercard | Authorization responses, settlement data | AWS Direct Connect; encrypted in transit |
| Core Banking Service | Arkéa | Settlement data, PIN blocks, key exchanges | Encrypted in transit (mTLS via Global Accelerator) |
| Arkéa | Core Banking Service | Key material (ZMK, KPVV, PEK, KCVV, IMK), settlement confirmations | Encrypted in transit (mTLS via Global Accelerator) |
| Core Banking Service | Exceet | Card personalization payloads (PAN, keys) | Encrypted in transit |
| Core Banking Service | Apata | 3DS authentication data (AAV values) | Encrypted in transit |
| Core Banking Service | AWS KMS | Key wrap/unwrap requests | AWS internal network (VPC endpoint) |
| Core Banking Service | AWS Payment Cryptography | Cryptographic 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 Cloud | Encrypted data for data warehouse | AWS 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:
| Component | Type | Region | Description |
|---|---|---|---|
| Core Banking Service | EC2 instances | eu-central-1 (primary), eu-west-3 (failover) | Monolith application processing all payment transactions. Stores, processes, and transmits CHD. |
| Aurora Global Database | Managed PostgreSQL | eu-central-1 (primary), eu-west-3 (replica) | Primary persistent storage for all cardholder data. |
| AWS KMS | Managed HSM | eu-central-1 | AES-256 KEK for data key wrapping. Used for internal storage encryption. |
| AWS Payment Cryptography | Managed HSM | eu-central-1 | TDES/AES operations for partner communication (PIN blocks, CVC, cryptograms). |
| VPC Infrastructure | Networking | eu-central-1, eu-west-3 | Subnets (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:
| Component | Type | Description | Justification for In-Scope |
|---|---|---|---|
| AWS CloudFront | CDN / Entry point | Public-facing entry point for HTTPS traffic. | Routes traffic to the CDE via ALB. |
| AWS Global Accelerator | Network entry point | TCP/UDP entry point on port 443. Used for mTLS connections (e.g., Arkéa). | Routes traffic to the CDE via ALB. |
| AWS WAF / Shield | Security control | DDoS protection and web application firewall. | Provides perimeter security for the CDE. |
| AWS Application Load Balancer | Load balancer | Distributes traffic to EC2 instances. Only accepts connections from CloudFront, Mastercard (Direct Connect), and Global Accelerator. | Direct network path to CDE compute. |
| AWS Network Load Balancer | Load balancer | Provides ClickHouse Cloud access to database replication. | Connects to CDE database (encrypted data only). |
| AWS Direct Connect | Network service | Provides Mastercard connectivity to the ALB. | Carries CHD/SAD between Mastercard and the CDE. |
| Tailscale Mesh VPN | Remote access | Admin/developer access to VPC and application servers. | Provides authenticated access into the CDE. |
| CI/CD Pipeline | Deployment | Deploys application code to CDE EC2 instances. | Impacts configuration and security of CDE systems. |
| AWS CloudTrail | Audit logging | Logs all API calls across the AWS environment. | Provides security monitoring for the CDE. |
| AWS IAM | Identity management | Controls 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:
| Component | Justification |
|---|---|
| Public APIs | Receive only tokens and masked data. No clear CHD reaches these systems. Segmented from CDE via security groups. |
| Reporting Pipelines | Process only tokenized and aggregated data. No clear CHD. |
| Customer Support Tools | Access only masked PAN (first 6 / last 4) and encrypted metadata. No clear CHD. |
| Third-Party Analytics | Receive 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 Environment | Separate AWS account. Does not store, process, or transmit production cardholder data. |
3.8 Segmentation Controls
| Control | Type | Purpose | Verification |
|---|---|---|---|
| AWS Security Groups | Logical (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 Grants | Logical (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 / Shield | Logical (perimeter security) | DDoS protection and web application firewall at the CloudFront and Global Accelerator entry points. | WAF rules and Shield configuration reviewed. |
| Separate AWS Accounts | Logical (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 Connect | Physical (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
| TPSP | Account Data Shared | Purpose | PCI DSS Assessed? | Direct CDE Access? | Connection Type |
|---|---|---|---|---|---|
| Mastercard | PAN, SAD (cryptograms, CVV, transaction data) | Payment network routing for authorization, capture, settlement, chargebacks, refunds | Yes | Yes | AWS Direct Connect to ALB |
| Arkéa | PAN, PIN blocks, settlement data, cryptographic key material (ZMK, KPVV, PEK, KCVV, IMK, KAAV) | Principal member, settlement bank, SEPA/instant payment processing, key provisioning | Yes | Yes | mTLS via Global Accelerator |
| Exceet | Personalization payloads (PAN, keys) | Card manufacturing and personalization | Requires verification | Yes | Via CBS over encrypted channel |
| Apata | AAV values (3DS authentication data) | 3D Secure authentication provider for online transactions | Requires verification | Yes | Via CBS over encrypted channel |
| AWS | All 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, S3 | Yes (SOC 2, PCI DSS) | Yes | Native 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 Type | Count | Location | Description |
|---|---|---|---|
| AWS Data Center (Primary) | 1 | eu-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) | 1 | eu-west-3 (Paris, France) | Failover region. Hosts Aurora replica. Switchover via Global Accelerator and CloudFront in case of primary region failure. |
| AWS Global Services | — | us-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
| Function | Description |
|---|---|
| Software Engineering | Development, maintenance, and deployment of the Core Banking Service. Engineers have access to the CDE via Tailscale for deployment and debugging. |
| Infrastructure / DevOps | Management of AWS infrastructure, security groups, VPC configuration, CI/CD pipelines, and Tailscale access policies. |
| Compliance / Security | Oversight of PCI DSS compliance, security monitoring (CloudTrail, Vanta), access control policies, and vendor risk management. |
| Executive Management | Strategic oversight of compliance posture and risk acceptance. |
4. Scope Confirmation
Based on the activities performed in this scoping review:
- All data flows for the payment stages (authorization, capture, settlement, chargebacks, refunds) and acceptance channels (card-present chip/contactless, card-not-present e-commerce/MDES, ATM) have been identified and are documented.
- The data flow diagram is current as of 2026-04-02 (version 0.1, pending formal approval).
- All locations where account data is stored (Aurora Global Database), processed (Core Banking Service, AWS KMS, AWS Payment Cryptography), and transmitted (Mastercard via Direct Connect, Arkéa via mTLS, Exceet, Apata, AWS services, Aurora replication) have been identified.
- All system components in the CDE and connected-to/security-impacting systems have been identified and documented.
- All segmentation controls (AWS Security Groups, Tailscale Grants, AWS WAF/Shield, separate AWS accounts, Direct Connect) have been identified and their effectiveness confirmed.
- All third-party service provider connections (Mastercard, Arkéa, Exceet, Apata, AWS) have been identified.
- All identified data flows, account data locations, system components, segmentation controls, and third-party connections are included in the defined PCI DSS scope.
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
| # | Action | Owner | Due Date | Status |
|---|---|---|---|---|
| 1 | Finalize and formally approve the cardholder data flow diagram (version 0.1 → 1.0) | Core Banking Team / CISO | Before QSA assessment | Open |
| 2 | Verify PCI DSS assessment status for Exceet and Apata (AOC collection) | Compliance | Before QSA assessment | Open |
| 3 | Complete the cardholder data flow diagram process document | Core Banking Team | Before QSA assessment | Open |
| 4 | Complete the cardholder network diagram process document | Core Banking Team | Before QSA assessment | Open |
| 5 | Schedule next semi-annual scope review | Compliance | 2026-10-05 | Open |
6. Sign-Off
| Role | Name | Signature | Date |
|---|---|---|---|
| Reviewer | Enrico — 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
- Explicit approval by authorized parties.
- Acceptable uses of the technology.
- List of products approved by the company for employee use, including hardware and software.
Evidence
Green-Got covers acceptable use of end-user technologies through its security awareness program and supporting security policies:
- Security Awareness Training documents that Green-Got assigns Vanta’s General security awareness module to all personnel through the Employees group, tracks completion in Vanta People, and requires annual recurrence.
- The Human Resource Security Policy requires all employees and relevant third parties to complete security awareness training at hire and annually thereafter. It also requires personnel to understand relevant information security and data privacy policies and procedures.
- The Asset Management Policy requires acceptable-use rules for information, assets, and information processing facilities to be documented in the Information Security Policy.
- The Information Security Policy defines acceptable use for company and customer information on electronic and computing devices. It limits access, use, and sharing of Green-Got proprietary information to authorized activity required for assigned job duties, requires personnel to use good judgment for personal use of company-provided devices, and permits authorized monitoring and audit of systems and network traffic.
- The Information Security Policy also defines unacceptable use and requires documented management approval for any legitimate business exception to the general restrictions. It expressly prohibits unlicensed software, unauthorized copying, non-business access to data, servers, or accounts, and unauthorized or illegal activity using Green-Got resources.
Approved products and assets are maintained through submitted evidence:
- Asset inventory and Comprehensive and appropriate asset inventory document that Green-Got tracks in-scope systems, employee devices, and other business assets in Vanta Inventory.
- Vendor Due Diligence and Vendor Risk Assessments document that third-party products and services are evaluated and tracked in Vanta Vendors before use where they affect Green-Got security, privacy, compliance, or the cardholder data environment.
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:
- criteria and expectations being used
- results of the review
- formal sign offs when completed
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 Type | Storage | Business Justification |
|---|---|---|
| PIN | Persistent (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/CVC2 | Persistent (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 data | Not stored (ephemeral only) | Processed in-memory during transaction authorization only (magstripe, for international compatibility). Never persisted. |
| PIN blocks | Not stored (ephemeral only) | Generated on-demand for Exceet manufacturing payload. Never persisted. |
| ARQC/ARPC, AAV cryptograms | Not 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.
- At rest: AES-256 KEK is stored in AWS KMS HSM. Plaintext data keys exist transiently in application memory only for encryption/decryption operations and are never persisted; only ciphertext and the encrypted data key are stored in PostgreSQL.
- In memory: Clear SAD exists transiently in Core Banking Service (CBS) memory only when required (display, partner communication, cryptographic operations). Plaintext data keys are likewise present only transiently for cryptographic operations, and both are zeroized immediately after use.
- Access control: Runtime KMS encrypt/decrypt operations are limited to least-privilege IAM service roles. MFA is required for human administrative access to KMS resources, such as key policy or configuration changes.
- Audit trail: All KMS encryption/decryption operations and administrative changes are logged via CloudTrail to an immutable S3 bucket.
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.
- The primary key-encryption key for stored account data is an AWS KMS multi-region symmetric key created for
ENCRYPT_DECRYPTusage with automatic rotation enabled. The Pulumi definition creates this as theencryption-keyand defines a KMS key policy that delegates usage through AWS IAM, which is the access-control boundary for encryption and decryption operations. - The application stores the KMS key identifier in configuration and uses that identifier when requesting
GenerateDataKeyandDecryptoperations from AWS KMS. The application does not store the KMS master key material in the repository or database. - Stored account data uses envelope encryption. The application maintains an in-memory data key cache refreshed through AWS KMS every 15 minutes. Each cached entry contains a plaintext data key for active encryption work and a KMS-encrypted data key blob for storage. The application encrypts the payload locally with ChaCha20-Poly1305 and persists only
[encrypted_key_len][encrypted_data_key][encrypted_payload]. The encrypted payload includes the nonce and authentication tag. This means the decrypting key material stored with account data remains in AWS KMS protected form. - The cached data key implementation keeps the plaintext data key only in process memory for active encryption work, wraps both plaintext and encrypted key buffers in zeroizing types, and stores the encrypted key blob for persistence alongside ciphertext.
- Database fields that contain PAN, PIN, CVC, and other sensitive values are wrapped in the
Encrypted<T>type. That type encrypts values before writing them to PostgreSQL and decrypts them only by calling AWS KMS with the configured KMS key ID. The encrypted data key is stored with the ciphertext; clear key material is not stored in the database.
In summary, the cryptographic keys used to encrypt or decrypt stored account data are protected as follows:
- The key-encryption key remains in AWS KMS HSM-backed storage and is not exported to the application or database.
- The per-use data keys persisted with stored account data are kept only as KMS-encrypted blobs.
- Access to use those keys is mediated through AWS KMS key policy plus AWS IAM permissions.
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:
- Operating Systems
- Cloud Infrastructure and Services
- Servers or Compute Instances
- Endpoint Software
- Network Infrastructure (Firewalls, VPCs, Security Groups)
- Databases
Acceptable evidence can include:
- A configuration baseline document outlining security settings for each in-scope system.
- Screenshots or reports from security tools (e.g., CIS Benchmark scans, AWS Security Hub findings) showing compliance with baseline standards.
- Documentation of access controls, logging configurations, and system monitoring settings.
- A simple spreadsheet listing key configurations and security settings applied across systems. Implementation Guidance for Startups: If you don’t have a mature IT team, start with these steps:
- Identify what you use – List the technologies you rely on (e.g., AWS, Linux, Windows, databases, firewalls).
- Use existing templates – Download CIS benchmarks or NIST STIGs relevant to your technology stack. Many security tools (e.g., AWS Security Hub, Microsoft Defender) provide default recommendations.
- Apply basic hardening – Disable unnecessary services, enforce multi-factor authentication (MFA), enable logging, and restrict admin access.
- Document in a simple format – A Google Doc or spreadsheet listing key security settings is enough to start. Track what’s configured, who owns it, and when it was last reviewed.
- Set up alerts & reviews – Use built-in cloud security tools (AWS Config, Azure Security Center) to monitor compliance, and schedule periodic checks (quarterly or biannually) to keep settings up to date. Audit Consideration: During your audit window, be prepared to re-upload evidence for at least a 10% for your interconnected system components, as randomly selected and requested by your auditor. A template can be found here: Google docs template
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:
| Principle | Application |
|---|---|
| Minimize attack surface area | Engineering work reduces exposed interfaces, services, permissions, and entry points to the minimum needed for the system purpose. |
| Establish secure defaults | Systems are configured with restrictive defaults and require explicit approval or configuration for elevated access or broader exposure. |
| Least privilege | Users, services, and systems operate with the minimum access required for their assigned function. |
| Defense in depth | Security controls are layered across application, infrastructure, identity, monitoring, and operational processes. |
| Fail securely | Authentication, authorization, validation, and processing failures result in a denied or protected state. |
| Do not trust services | Integrations and service boundaries are treated as untrusted unless authenticated, authorized, and validated. |
| Avoid security by obscurity | Security decisions rely on explicit controls and reviewable configurations instead of hidden implementation details. |
| Keep security simple | Engineering designs favor clear, maintainable controls that teams understand and operate consistently. |
| Fix security issues correctly | Security remediation addresses the underlying weakness and includes validation that the issue is resolved. |
Green-Got also applies the following privacy-by-design principles:
| Principle | Application |
|---|---|
| Proactive not reactive; preventative not remedial | Privacy and security risks are addressed during design and implementation rather than only after incidents. |
| Privacy as the default setting | Personal data handling defaults to limited collection, limited access, and controlled processing. |
| Privacy embedded into design | Data protection is part of system design, engineering review, and implementation decisions. |
| Full functionality, positive-sum | Security and privacy controls are designed to support business functionality while protecting users and data. |
| End-to-end security, full lifecycle protection | Data protection requirements apply from collection through processing, storage, retention, and deletion. |
| Visibility and transparency | Security and privacy controls are documented and reviewable through policies, procedures, and engineering records. |
| Respect for user privacy | Engineering 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:
- Annual training modules — Vanta’s built-in trainings are assigned via Vanta People Management groups. The Employees group receives General security awareness, Insider threat, PCI DSS, and Social engineering trainings. The Developers group additionally receives Secure code training. Vanta maintains and updates training content to address current threats and evolving security practices.
- Completion tracking — Vanta People tracks completion per person and enforces annual recurrence. Overdue trainings are flagged automatically.
- Phishing simulations — Green-Got runs simulated phishing campaigns through Riot to test personnel awareness and reinforce training content. The Riot campaign evidence for the last 90 days shows 100% coverage, with 47 employees attacked, 4 employees compromised, and 34 employees reporting a threat. This corresponds to a 9% vulnerability rate and a 72% reporting rate.
Green-Got maintains the Riot phishing campaign dashboard as supporting evidence for phishing simulation coverage and reporting performance.
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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-04-21 |
| Last Updated | 2026-05-01 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual 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
| Attribute | Implementation |
|---|---|
| Vendor | AWS Systems Manager Parameter Store, AWS KMS, and the Green-Got application runtime |
| Type of system/process | Application-side keyed HMAC-SHA256 PAN lookup value generation and verification using startup-loaded key material |
| PAN input | Entire PAN |
| Cryptographic method | Keyed HMAC-SHA256 |
| Key strength | 256 bits |
| Key type | Application-managed HMAC secret stored as AWS Systems Manager Parameter Store SecureString values |
| Production storage | Production AWS account active and secondary PAN lookup HMAC SecureString values |
| Non-production storage | Non-production AWS account active and secondary PAN lookup HMAC SecureString values |
| Purpose | PAN lookup and matching without decrypting stored PAN |
| Cryptoperiod | 12 months from activation date for the active production PAN lookup HMAC key |
| Rotation window | Maximum 30 calendar days from secondary-key publication to previous-key retirement, including lookup slot backfill and verification |
| Access control | IAM-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 disclosure | The keyed HMAC lookup value is not displayed to operators or customers and is not returned in customer-facing APIs |
| Rotation model | Two 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:
- A PAN is created in the CDE.
- The application generates a keyed HMAC-SHA256 lookup value from the clear PAN using the active startup-loaded PAN lookup HMAC key.
- The application encrypts the PAN before storing it in the database.
- The encrypted PAN envelope and the keyed HMAC lookup value are stored with the PAN record.
- 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.
- 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
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-04-23 | julian@green-got.com | Document PAN lookup HMAC hashing and deterministic lookup controls. |
| 1.1 | 2026-04-30 | etienne@green-got.com, julian@green-got.com | Align 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.2 | 2026-05-01 | julian@green-got.com | Clarify 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:
- Details of all algorithms, protocols, and keys used for the protection of cardholder data, including key strength and expiry date,
- Description of the key usage for each key, and
- Inventory of any HSMs and other SCDs used for key management If CHD is not stored and encryption is not used to protect CHD, use the ‘Deactivate’ button and include an explanation.
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
- Human interactive access is performed exclusively through AWS Identity Center (SSO) with individual user identities. This is documented in Access and IAM Credentials Rotated.
- Application and infrastructure workloads run under IAM roles assumed by AWS services and ECS tasks, not under shared or human-login accounts.
- CI/CD automation uses a single IAM user (
cicd) defined as an automation-only identity. This account has no console password and no interactive login profile.
Applicability of the required controls
Since no service account has interactive login capability:
- Interactive use is prevented — no service account is provisioned with console access or interactive credentials.
- Time-limited interactive use — not applicable; no exception window for service-account interactive login exists.
- Documented business justification — not applicable; no interactive use of service accounts occurs.
- Management approval — not applicable; no interactive use of service accounts has been requested or granted.
- Individual identity confirmation — not applicable; service accounts are never accessed interactively.
- Action attribution — all human actions are attributable to individual users through AWS Identity Center, and all automated actions are attributable to their dedicated non-interactive service identities.
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:
- Identifying phishing emails, pretexting, and other social engineering techniques.
- Reacting to suspected social engineering attempts.
- Reporting suspected phishing and social engineering activity through the appropriate channels.
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:
- GitHub Dependabot monitors repository dependencies continuously and raises alerts when known vulnerabilities are discovered.
- The custom CI
Dependency Scancheck runscargo-denybefore merge on dependency-impacting pull requests. The check evaluates the locked dependency graph against the RustSec advisory database and the repository’s accepted advisory baseline, and blocks new unaccepted vulnerable, yanked, or unmaintained dependencies from being introduced.
Remediation of dependency vulnerabilities is automated through a production process:
- 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
- Amazon Inspector continuously scans container images and EC2 hosts for known vulnerabilities.
- Scanner outputs are consolidated in Vanta Vulnerabilities alongside GitHub Dependabot findings, as described in Vulnerability scan.
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:
- Your cloud platform provider (e.g., AWS, Azure, GCP)
- Your database provider (e.g., Snowflake)
- Your monitoring or infrastructure services (e.g., Datadog, Cloudflare) The agreement should show how responsibilities for security controls, incident response, data protection, and system availability are shared between your organization and the provider.
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:
- AWS Customer Agreement: The standard agreement governing the use of AWS services, including security and compliance obligations for both parties. Available at https://aws.amazon.com/agreement.
- AWS Shared Responsibility Model: Defines the division of security responsibilities between AWS (security of the cloud — physical infrastructure, hypervisor, networking) and Green-Got (security in the cloud — application configuration, access management, data encryption). Available at https://aws.amazon.com/compliance/shared-responsibility-model.
- AWS PCI DSS Compliance: AWS maintains PCI DSS Level 1 certification. The list of AWS services in scope for PCI DSS is published at https://aws.amazon.com/compliance/services-in-scope/PCI. The AWS Attestation of Compliance (AOC) is available in Vanta.
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:
- Tool name - Screenshot of dashboard for network and services administration
- List of network segments
- ACL and firewall rules in place for network segment(s)
- List of all user accounts with access to administrate segments (PCI 2.2.2; 8.1; 8.2.2)
- 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
- Audit log settings (PCI 10.2)
- Centralized authentication settings (PCI 8.x)
Guidance
- Pending inventory with list of network/services population - 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
- We are using Infrastructure as code to manage this. The tool is called Pulumi but there is no dashboard
- This is described in
- Part of Infrastructure as code
- Part of Infrastrucure as code and controlled via Tailscale Grants
- There are no passwords involved in this process
- Infrastructure as code follows the same change management procedure as Change Management Procedures
- 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:
- 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. 3-) Centralized authentication settings (PCI 8.x) (if applicable) 4-) Any other organizationally enforced workstation hardening configurations/policies
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:
- 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).
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:
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:
- Calendar invites or scheduling records showing your upcoming/completed Incident Response Plan tests.
- Meeting attendance records, sign-in sheets, or participant lists confirming involvement of key stakeholders.
- Meeting minutes or a post-exercise report summarizing the scenario tested, key actions taken, and findings.
- Screenshots, slides, or recorded sessions used during the tabletop exercise.
- A documented after-action report highlighting gaps identified and improvements needed.
- Remediation plans, assigned action items, or tracked follow-ups addressing weaknesses identified as a result of the test.
- Evidence of leadership sign-off or acknowledgment of the test results. Sample tabletop document: Google docs template / Docx template
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.
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:
- NIST: time.nist.gov
- Microsoft: time.windows.com
- Google: time.google.com
- 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:
| Policy | Value | Effect |
|---|---|---|
| Authenticator Attachment | CrossPlatform | Requires an external mobile device, not a browser-embedded credential |
| User Verification | Required | The authenticator verifies the user’s identity (biometric or PIN) before signing |
| Resident Key | Required | The passkey is stored on the operator’s mobile device |
| Credential Hints | Hybrid | Cross-device flow via the operator’s mobile phone (QR code scan) |
| Allowed Transports | Hybrid only | USB and Internal transports are explicitly filtered out, enforcing mobile-only authentication |
These settings ensure that each authentication factor is:
- Physically bound to the operator’s personal mobile device
- User-verified via biometric or PIN at each use
- Not exportable or shareable between users
- Mobile-only — hardware security keys (USB) and browser-embedded credentials are not accepted
[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:
- An operator from registering a second passkey while one is already active
- Multiple pending passkeys from existing simultaneously for the same operator
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:
| Field | Description |
|---|---|
id | Unique passkey identifier |
operator_id | Foreign key to the operator (user) who owns this passkey |
credential | The WebAuthn credential data (public key, counter, etc.) |
credential_id | Derived from the WebAuthn credential, unique per authenticator |
device | Description of the authenticator device |
status | One of: Pending, Active, Rejected, Deleted |
last_used_at | Timestamp 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
- 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.
- The operator initiates passkey registration. A WebAuthn challenge is generated with a 6-minute TTL and is consumed once used.
- The operator completes the WebAuthn ceremony on their mobile device (biometric/PIN verification occurs on-device).
- The passkey is created in
Pendingstatus — it does not grant login access.
3.2 Admin Approval (Dual Control)
- A team admin or super admin reviews the pending passkey and approves or rejects it.
- 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
- 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:
- The passkey status transitions to
Deleted - All active sessions for that operator are immediately deleted from the
bo_sessiontable - 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:
- The operator’s
archived_attimestamp is set - All of the operator’s passkeys (both
PendingandActive) are cascaded toDeletedstatus, with the audit trail recordingArchiveOperatorCascadeas the source - 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:
- Tailscale access is revoked — the operator loses network-level access to the back office
- Slack access is revoked — the operator loses access to internal communications
- All Google-authenticated tools are revoked — no tool that relies on Google SSO remains accessible
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:
- The operator initiates login from a Tailscale-connected device.
- The server generates a WebAuthn authentication challenge scoped to the operator’s registered credential.
- The operator’s mobile device verifies the user (biometric or PIN) and signs the challenge.
- The server validates the signed response against the stored public key for that operator.
- A session is created with an HMAC-SHA256 signed token encoding the
session_idandoperator_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
| Control | Configuration |
|---|---|
| Token Signing | HMAC-SHA256 with a server-side signing key |
| Token Content | session_id + operator_id (protobuf-encoded, base64url) |
| Cookie Flags | HttpOnly, Secure, SameSite=Strict (production) |
| Session Expiry | 12 hours from creation |
| CSRF Protection | Per-session CSRF token |
| Logout | Deletes 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:
| Field | Description |
|---|---|
passkey_id | The passkey that was modified |
modified_at | Timestamp of the change |
modified_by_kind | Operator or System |
modified_by_operator_id | The admin who performed the action (if operator-initiated) |
modified_by_name | Name of the actor |
modified_to_status | The new status (Active, Rejected, Deleted) |
modified_to_reject_reason | Reason 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
| Control | Mechanism |
|---|---|
| One passkey per operator | Database unique partial index on operator_id |
| Cryptographic binding | WebAuthn credential is bound to operator’s user ID and relying party |
| User verification | Biometric or PIN required at each authentication |
| Non-exportable key | Private key never leaves the operator’s mobile device |
| Dual-control activation | Admin approval required; self-approval blocked |
| Scoped administration | Team admins operate within their team; super admins operate company-wide |
| Immediate revocation | Passkey deletion and operator archival immediately invalidate all sessions |
| Offboarding cascade | Operator archival triggers Google account deletion, revoking Tailscale and all SSO access |
| Audit trail | Every status change is logged with actor identity and timestamp |
| Session isolation | HMAC-signed, per-operator, time-limited sessions |
| Network-level identity | Only 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:
- There is no default root password. The
rootaccount is locked by default and direct root login is disabled. - The default user (
ec2-user) has no password set. - Password-based authentication is disabled by default in
sshd_config.
Additionally, our infrastructure makes SSH access architecturally impossible. The Pulumi infrastructure-as-code configuration (src/infrastructure/environment/setup_region.ts) enforces:
- No SSH key pairs — The EC2 Launch Template does not specify a
keyName. No SSH key pairs are created or managed in the infrastructure. - 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.
- 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:
- Manage users on Linux instances — “By default, password authentication and root login are disabled, and sudo is enabled. To log in to your instance, you must use a key pair.”
- Amazon Linux 2023 User Guide
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:
- A unique auto-generated integer primary key (
id int primary key generated always as identity) - A unique email address enforced at the database level (
email text not null unique)
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:
- Extracts the session cookie
- Verifies the HMAC-SHA256 signature
- Queries the database to confirm the session exists and matches the operator
- 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.
- Google references: Sign in with Google, OpenID Connect
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.
- Render references: Login Settings, SAML Single Sign-On, Platform Compliance and Certifications
- Vanta evidence locations: Secure Password Settings - Vendors, Vendor inventory
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:
- Customer-created credentials configured during provisioning.
- Federated identity through SSO/OIDC.
- IAM roles, IAM policies, and customer-managed access grants.
- Customer-generated API keys, mTLS certificates, or service credentials.
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.
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:
- GitHub Dependabot monitors repository dependencies and raises security alerts for known vulnerable packages.
- The custom CI
Dependency Scancheck runscargo-denybefore merge on dependency-impacting pull requests to detect known vulnerable, yanked, or unmaintained open-source components introduced during development. - Vulnerability scanners integrated into Vanta track findings from GitHub Dependabot and Amazon Inspector outputs. Remediation history is visible in Vanta Vulnerabilities.
- Host and container exposure is also monitored through Amazon Inspector scanning and patching workflows described in Vulnerability Intelligence Sources, Vulnerability scan, and Critical security patches are installed within one month of release.
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:
- Open dependency vulnerabilities are tracked as GitHub Dependabot alerts and are also visible through Vanta vulnerability history.
- A production automation runs daily to check for open Dependabot alerts and create remediation pull requests for dependency-based issues that are directly remediated through package updates.
- A second production automation runs on weekdays to create focused dependency-update pull requests, which reduces the time unsupported or outdated libraries remain in use.
- Findings from Amazon Inspector remain visible in Vanta until remediated.
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:
- the severity assigned by the originating scanner or vendor advisory,
- whether the affected component is in the PCI DSS scope or otherwise impacts the security of the CDE,
- whether the issue affects custom-developed application behavior, a third-party dependency, or the underlying operating environment,
- whether exploitation is blocked by existing controls or requires immediate remediation.
Risk ranking follows the severity and exposure information from the authoritative source of the finding:
- Critical and high-risk findings affecting in-scope systems are handled first.
- Medium and lower-risk findings remain tracked until remediation and are prioritized after higher-severity items.
- For software and patch-related issues, Green-Got aligns remediation urgency with the requirement that critical security patches are installed within one month of release and with the secure development requirement to deploy materially impactful vulnerability fixes as early as possible.
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:
- Continuously, as GitHub Dependabot, Vanta-integrated scanners, and Amazon Inspector generate findings.
- Daily, through the production automation that checks for open Dependabot security alerts and starts remediation work.
- Every weekday, through the dependency currency automation that creates focused update pull requests.
- On dependency-impacting pull requests, through the custom CI
Dependency Scancheck usingcargo-deny. - Weekly for host operating system patch intake, because production hosts are rotated through AWS Auto Scaling instance refresh using current machine images.
- Annually, during the supported-systems and vendor-support review documented in Maintenance and use of only currently supported systems.
5. Roles responsible
The vulnerability management process uses the following roles already defined in Green-Got policies:
| Role | Responsibility 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 Risk | Oversees compliance with PCI DSS and validates that the vulnerability management process remains aligned with the broader risk and compliance program. |
| Cybersecurity Engineer | Supports risk assessment activities, documents relevant security risks, and escalates material exposure to leadership. |
| Developer/Systems Owner | Implements 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:
- Custom-developed applications: application changes go through pull-request review and pre-merge CI before deployment, and engineers remediate code defects through the normal review and deployment process.
- Third-party software and libraries: GitHub Dependabot, the custom CI
Dependency Scancheck usingcargo-deny, weekday dependency refreshes, and daily remediation automations identify and remediate vulnerable open-source and third-party components used by the monolith. - Operating systems and platform software: AWS-integrated scanning and weekly host refreshes address host-level patching, while the annual supported-systems review ensures unsupported operating systems and software are not retained in scope.
7. Resulting operating model
In practice, Green-Got’s vulnerability management procedure is:
- Detect new vulnerabilities from GitHub, Vanta-integrated scanners, pull-request scanning, and Amazon Inspector.
- Track each finding in the originating system until closure, with Vanta serving as the consolidated evidence trail for integrated scanners.
- Triage the finding by severity, PCI scope, exploitability, and affected layer.
- Remediate dependency issues through automated pull requests whenever possible, and remediate custom-code, configuration, and operating-system issues through the standard engineering change process.
- 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:
- Evidence that WAF is configured to protect public-facing web applications that impact the CDE
- Evidence that WAF logs are enabled and generating alerts
- Evidence that WAF is configured to automatically block or immediately alert on anomalous or malicious traffic
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:
- event_bus
- iso8583 or iso20022
- tailscale
Prefer DateTime<Utc> over NaiveDateTime and timestamptz over timestamp
-
DateTime<Utc>has a much better API, for example you can doUtc::now()to get aDateTime<Utc>forNaiveDateTimeyou need to doUtc::now().naive_utc, etc. -
DateTime<Utc>serializes with serde to the correct format for Javascript’s Date API,NaiveDateTimedoes not.
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
-
If you create your own error enum always use the
macros::errorderive macro. Your variants should never end withErrorinstead the name of your enum should. -
Don’t use
anyhow; useeyre. We are in the process of removinganyhow.eyrehas some nicer utilities and does a better job with backtracing compared toanyhow.
Reference to type that implements Copy
- Types that implement the
Copytrait should NOT be passed via a reference like this&Type. The size of the reference often exceeds the size of the Copy Type itself which means it’s less efficent than passing the type itself and it’s less ergonomic from a DX perspective.
Assertions
- Prefer structured assertions in tests when they express the check naturally: use
assert_eq,assert_ne, orassert_matches. - Use
assert!for direct boolean predicates. - The pretty-assertions crate is already in our
Cargo.lock; use it freely. For snapshot testing, consider insta or expect-test.
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:
- It forces unnecessary clones — The caller always pays the cost of cloning, even when they could have given up ownership of
T. - 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:
- Pass ownership (no clone needed)
- Explicitly call
.clone()when they need to keep the value
❌ 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):
| Partner | Role in the flow | PCI relevance |
|---|---|---|
| Exceet | Card manufacturing, personalization payload intake. | Receives PAN-derived data and card profiles. |
| Mastercard | Payment 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éa | Principal member, settlement, sponsor banking, SEPA and instant payment processing. | Provides EMV chip key material; handles card transaction clearing and SEPA/instant payment rails. |
| Apata | 3D Secure authentication provider for online transactions. | Validates e-commerce authentication (AAV). |
| AWS KMS | Managed 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 Cryptography | Managed 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:
- PANs are stored in a separate pool
- Cards reference PANs via
pan_id - Since card records link
pan_idwith cardholder name and expiration date, these fields are classified as CHD and must be encrypted
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:
- PIN: Not stored as persistent issuer-side data in the target design
- PVV: Stored encrypted with ChaCha20-Poly1305 using KMS envelope encryption for online PIN verification
- Issuer-side encrypted PIN block: Stored encrypted with ChaCha20-Poly1305 using KMS envelope encryption; the inner PIN block is retained under
PEK_MC_AUTHby AWS Payment Cryptography and translated on demand for reveal PIN and Exceet transmission - CVC: Stored encrypted with ChaCha20-Poly1305 using a KMS-generated 256-bit data key for issuer operations (validation, manufacturing)
- Transport PIN blocks: Ephemeral only when generated for network verification or manufacturer communication
- Cryptograms (ARQC/ARPC, AAV): Ephemeral only (validated during transaction, never persisted)
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:
-
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.
-
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.
-
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.
-
Tokenizing for external APIs: Public identifiers (PAN tokens, UUIDs) replace real PANs in API responses and logs.
-
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 element | Classification | Storage rule | Transit rule | Processing location |
|---|---|---|---|---|
| PAN (full) | CHD | Envelope-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-sensitive | Not stored; derived on-the-fly from encrypted full PAN. | TLS 1.2+. | CBS generates when needed for display. |
| PAN tokens | Non-sensitive | UUID/TSID stored in clear. | TLS 1.2+. | CBS and downstream services. |
| PIN | SAD | Not 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. |
| PVV | SAD | Encrypted 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 block | SAD | Persisted 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 block | SAD | Ephemeral 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/CVV | SAD | Encrypted 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) | SAD | Never 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) | SAD | Not stored; validated during transaction only. | TLS 1.2+ network↔CBS. | Validated by CBS using keys from Arkéa. |
| AAV (Account Authentication Value) | SAD | Not stored; validated during authorization only. | TLS 1.2+ Apata↔CBS. | Validated by CBS using shared HMAC-256 key from Arkéa. |
| Cardholder name, expiry | CHD | Encrypted at rest (CHD when stored with PAN reference). | TLS 1.2+. | CBS and reporting pipelines. |
| BIN (Bank Identification Number) | Non-sensitive | Plaintext. | TLS 1.2+. | Multiple services. |
3.1 Processing principles
- Arkéa is key source: All payment network cryptographic keys are generated by Arkéa and distributed to Green-Got and partners.
- Storage always uses ChaCha20-Poly1305 (KMS): All issuer-side persisted CHD and SAD reference data uses AWS KMS envelope encryption with ChaCha20-Poly1305. This includes PAN, PVV, the stored issuer-side encrypted PIN block, CVC, cardholder metadata, and related security state.
- Payment keys are not storage keys: PEKs, PVKs, and IMKs remain in AWS Payment Cryptography for PIN and partner operations. They are never the issuer’s data-at-rest encryption keys.
- Payment HSM operations are transport-specific: AWS Payment Cryptography is used to verify PINs, produce or translate protected PIN blocks for app or partner transport, and generate EMV issuer scripts.
- Green-Got owns issuer-side reference data: As the card issuer, Green-Got owns PANs, cardholder metadata, CVC, and PIN reference data such as PVV and the issuer-side encrypted PIN block. The target design does not rely on storing the clear PIN itself.
- Clear CHD/SAD minimized in CBS: Clear CHD exists only when absolutely required. PIN flows are designed around a dedicated iframe over TLS for app set/change and reveal display, PEK-protected network PIN blocks, and HSM-side verification rather than persistent clear or encrypted PIN storage in CBS.
- No CHD/SAD in logs: All logging implementations redact sensitive fields. CHD (PAN, cardholder name) and SAD (PIN, PIN blocks, CVC, PVV, cryptograms) are never logged. The
Encrypted<T>type prints****by default.
4. PCI DSS compliance boundaries
4.1 In-scope systems (CDE)
- AWS KMS: Stores the KEK and wraps/unwraps data keys for data-at-rest encryption.
- AWS Payment Cryptography: Used for 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.
- Core Banking Service (CBS): Stores encrypted CHD, orchestrates KMS/HSM calls, enforces access controls.
- Database (Postgres): Stores encrypted PAN, PVV, issuer-side encrypted PIN block, CVC, cardholder metadata, and card security state (all encrypted with ChaCha20-Poly1305 using KMS-generated data keys where applicable).
- Infrastructure: VPC, security groups, IAM policies, CloudTrail logging for CDE components.
4.2 Out-of-scope systems (non-CDE)
- Public APIs: Receive only PAN tokens, masked PANs, non-sensitive transaction metadata.
- Reporting pipelines: Access only tokenized or aggregated data.
- Customer support tools: Access only masked PAN and encrypted metadata (no decryption capability).
- Third-party analytics: Receive anonymized/tokenized datasets only.
4.3 Controls
- Code segmentation: CDE components isolated in private crates.
- Least privilege access: IAM roles grant minimal permissions. MFA required for KMS and AWS Payment Cryptography access.
- Immutable audit trail: CloudTrail logs all KMS and AWS Payment Cryptography API calls. Logs forwarded to immutable S3 bucket.
- Report reviews: Automated reports with access patterns.
- Data key rotation: KEK and data keys rotated according to PCI DSS schedules.
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
| Field | Value |
|---|---|
| Document Status | Submitted |
| Effective Date | 2026-02-10 |
| Last Updated | 2026-06-04 |
| Document Owner | Core Banking Team |
| Review Frequency | Annual or after a material cryptographic architecture change |
1. Guiding principles
-
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.
-
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.
-
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.
-
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.
-
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.
-
Minimum exposure — Clear CHD/SAD exists only transiently in CBS memory during partner communication or display operations, then immediately zeroized.
-
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:
- HSM operations (AWS KMS): KEK storage, data key generation, data key wrapping/unwrapping
- Application operations (Core Banking Service): ChaCha20-Poly1305 encryption/decryption using plaintext data keys
Algorithms:
- ChaCha20-Poly1305 (Authenticated Encryption with Associated Data) for data encryption (application-side)
- HMAC-SHA256 for PAN matching/lookup without decryption (application-side)
Key structure:
- KEK (Customer Master Key): Managed by AWS KMS HSM, never leaves the HSM, never shared with partners.
- Data keys: KMS-generated 256-bit data keys, wrapped by KEK (HSM operation), used in-application for ChaCha20-Poly1305.
- PAN lookup HMAC keys: Application-managed 256-bit HMAC keys stored in AWS Systems Manager Parameter Store and loaded into CBS memory at startup. One key is active for production lookup generation, and one key is staged as the secondary rotation key.
- Encryption flow: KMS generates data key and wraps it with KEK (HSM). Application receives plaintext and encrypted data key, encrypts data with ChaCha20-Poly1305 (in-application), stores encrypted data key alongside ciphertext, then zeroizes plaintext data key.
Use cases:
- PAN encryption at rest in PostgreSQL
- PAN matching: keyed HMAC-SHA256 lookup value derived from PAN and used as an index to find encrypted PAN records when Mastercard sends clear PAN in transactions (allows lookup without decryption)
- PIN reference data encryption at rest in PostgreSQL (for example PVV and the issuer-side encrypted PIN block retained under
PEK_MC_AUTH) - Cardholder metadata (name, address, phone)
- Any CHD or SAD requiring persistence in Green-Got’s database
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:
- Key strength: 256 bits
- Cryptoperiod: 12 months from activation date for the production active key
- Rotation window: maximum 30 calendar days from publication of the secondary key to retirement of the previous key
- Environment separation: production and non-production PAN lookup HMAC keys are distinct secrets stored in separate AWS accounts and must never be reused across environments
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:
- TDES (Triple DES, 168-bit): Legacy algorithm for partners like Exceet
- AES (AES-256): Modern algorithm for partners like Mastercard (for certain operations)
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:
- Online PIN verification: verify inbound encrypted PIN blocks against issuer-side PIN reference data
- Set PIN app flow: the dedicated app iframe handles PIN entry and sends the selected PIN to CBS over TLS; CBS builds an ISO-4 PIN block protected by an ECDH-derived key, translates it under
PEK_MC_AUTHusing the Mastercard-required PIN block format, and then calls AWS Payment Cryptography to generate the PVV and issuer PIN block - Reveal PIN app flow: CBS performs the AWS Payment Cryptography reveal operation and renders the clear PIN only inside the dedicated iframe response
- Exceet (TDES): generate manufacturer PIN blocks and protect card personalization payloads
- Mastercard PIN transport (TDES or network-specific mode): handle inbound online PIN blocks and ATM PIN management flows
- EMV issuer scripts: generate script MAC and encrypted PIN change data for offline chip updates
- 3DS AAV validation (HMAC-SHA256): verify authentication values generated by Apata using shared Arkea-provided HMAC key
Key source: All keys provided by Arkéa and imported into HSMs.
Critical distinctions:
- AWS Payment Cryptography is not used to encrypt data for internal storage (that uses AWS KMS-backed envelope encryption)
- It’s used for payment HSM operations such as verification, translation, transport protection, and EMV secure messaging
- Partners cannot decrypt data encrypted with Green-Got’s AWS KMS-backed envelope encryption keys
- For partner communication, shared keys from Arkéa must be used (whether TDES or AES)
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:
- Arkéa generates all cryptographic keys and distributes to Green-Got and partners
- Two separate HSM services with distinct purposes:
- AWS KMS: HSM-backed key management (KEK + data key generation/wrapping). CBS performs ChaCha20-Poly1305 encryption in-application using these keys.
- AWS Payment Cryptography: HSM-based cryptographic operations for PIN flows, transport protection, and partner communication with Arkea-provided keys
- Two-stage encryption pattern: Data stored with KMS, decrypted in memory, re-encrypted via Payment Cryptography for partner transmission
- Clear CHD/SAD exists only in CBS memory during authorized operations, immediately zeroized after use
3. Key management
3.1 Key sources and ownership
| Key Type | Generated By | Distributed To | Purpose | Algorithm |
|---|---|---|---|---|
| KEK (KMS) | AWS KMS | Green-Got only | Master key for wrapping data keys | AES-256 |
| Data Keys | AWS KMS (wrapped by KEK) | Never exported | Encrypt individual data elements with ChaCha20-Poly1305 | 256-bit symmetric data keys |
| ZMK (Zone Master Key) | Arkéa | Green-Got, Exceet | Wrap/unwrap partner TR-31 key blocks | AES-256 or TDES depending on the Arkéa/partner domain |
| PVK_VISA (PIN Verification Key, TR-31 V2) | Arkea | Green-Got only | Generate and verify Visa PVV for online PIN verification | TDES |
| PEK_MC_AUTH (PIN Encryption Key, TR-31 P0) | Arkea / network domain | Green-Got and network transport boundary | Protect inbound PIN blocks in Mastercard authorization and ATM PIN management flows | TDES |
| PEK_EXCEET (PIN Encryption Key, TR-31 P0) | Arkea | Green-Got, Exceet | Protect outbound manufacturer PIN blocks and personalization payloads | TDES |
| KCVV (CVV/CVC Key, aka KCVX) | Arkéa | Green-Got only | CVC operations with partners if needed (wrapped by ZMK); not used for internal storage - Green-Got uses KMS ChaCha20-Poly1305 for storage | TDES |
| IMK Set (Issuer Master Keys) | Arkéa | Green-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éa | Green-Got, Apata | 3D Secure authentication value validation | HMAC-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:
- Internal storage (PostgreSQL): Always uses KMS envelope encryption (AES-256 KEK + data keys, ChaCha20-Poly1305 for data) for PAN, PVV, the issuer-side encrypted PIN block retained under
PEK_MC_AUTH, CVC, cardholder metadata, and security state - Payment cryptographic operations and partner communication: Uses Arkea keys via AWS Payment Cryptography (TDES, AES, or ECDH depending on the flow)
- Key point: Arkea payment keys (KCVX, PVK, PEK, IMK) are stored in AWS Payment Cryptography for PIN and partner operations only, never for internal storage encryption
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:
- ZMK: 3 components (XOR’d together to form master key)
- PVK, PEKs, IMK set: TR-31 wrapped blocks (encrypted under ZMK)
- AAV Key: Shared HMAC-256 key (distributed to both Green-Got and Apata)
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)
- Receive TR-31 wrapped key blocks from Arkéa
- Import to AWS Payment Cryptography using ZMK ARN as unwrapping key:
| Key | TR-31 Key Type | Purpose |
|---|---|---|
| PVK_VISA | V2 | Visa PVV generation and verification |
| PEK_MC_AUTH | P0 | Mastercard authorization and ATM PIN transport |
| PEK_EXCEET | P0 | Manufacturer communication and chip personalization |
| KCVX | Card verification key | CVC operations with partners if needed (not used for internal storage) |
| IMKac | MAC key, ISO-9797 | ARQC/ARPC cryptograms |
| IMKsmi | MAC key, ISO-9797 | Script MAC |
| IMKsmc | MAC key, ISO-9797 | CVC derivation |
| IMKidn | MAC key, ISO-9797 | Identifier key (not required) |
- 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:
- KMS data keys are generated once and cached in memory for 15 minutes
- Encryption operations use the cached key (synchronous, no network calls)
- Background task refreshes the cache every 15 minutes
- The encrypted data key is stored with each ciphertext for later decryption
Benefits:
- Performance: Encryption is synchronous (no KMS network call per operation)
- Cost: Reduces KMS API calls by ~99% (from per-encryption to once per 15 minutes)
- Resilience: Continues working during brief KMS outages using cached keys
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:
- Global cache (
KMS_DATA_KEY_CACHE): Thread-safe singleton usingArcSwapfor lock-free reads - Atomic refresh: Background task swaps in new keys every 15 minutes without blocking readers
- Graceful degradation: If refresh fails, continues using existing cached key while retrying with exponential backoff
- Startup retry logic: Aggressive retry (1s → 2s → 4s → 8s → 16s → 30s max) until first key is generated (critical for app startup)
- Refresh retry logic: On failure, retries every 1 minute with exponential backoff up to 15 minutes
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:
- Clear PAN exists only transiently in memory during authorized operations, then immediately zeroized
- Decryption is lazy:
Encrypted<T>stores ciphertext until.expose()or.into_inner()is called - Each decryption requires a KMS API call (cannot use cache - must decrypt the specific data key stored with that ciphertext)
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:
- PANs are stored in a separate
panspool table, referenced bycards.pan_idforeign key - This allows centralized PAN management and potential PAN reuse across cards
- The pattern is the same regardless of algorithm (TDES or AES). The difference is which key and algorithm you use in AWS Payment Cryptography
- All partner keys come from Arkéa
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:
- AAV generated by Apata using shared HMAC-256 key from Arkéa
- CBS validates AAV using the same shared key via AWS Payment Cryptography VerifyMac API
- AAV never stored (transient validation only)
- Modern cryptography (HMAC-256), not TDES
- Different from the application-managed PAN lookup HMAC keys stored in AWS Systems Manager Parameter Store and used internally for PAN matching/lookup
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:
- Card credentials generated or selected during issuance: PAN and CVC are issuer-generated; the cardholder selects the PIN for physical cards
- Storage encryption: PAN, CVC, PVV, the issuer-side encrypted PIN block retained under
PEK_MC_AUTH, and cardholder metadata are encrypted with KMS-backed envelope encryption for storage - Issuer-side PIN baseline: Visa PVV is generated via AWS Payment Cryptography and stored as issuer reference data for online verification
- PIN block for manufacturer: Manufacturer PIN block is generated on demand by translating the stored issuer-side PIN block from
PEK_MC_AUTHtoPEK_EXCEET - Reveal PIN and online verification: Both remain possible without storing the clear PIN itself in PostgreSQL
- Authoritative storage: Green-Got stores issuer-side reference data plus a single issuer-side encrypted PIN block retained under
PEK_MC_AUTH, not the clear PIN - TDES keys available but not used for storage: KCVX (CVC) key from Arkéa is available but not currently used for internal storage - may be used for partner operations if needed
6. Key rotation
6.1 KEK rotation (AWS KMS)
Frequency: Annual (automated by AWS KMS)
Process:
- AWS KMS automatically rotates KEK yearly
- Old KEK versions retained for decrypting existing data
- New encryptions use new KEK version
- No application changes required (transparent rotation)
Data key rotation impact:
- Background refresh automatically gets new data keys encrypted under the new KEK version
- Within 15 minutes of KEK rotation, all new encryptions use the new KEK
- Old data remains encrypted under previous KEK versions (still decryptable)
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:
- Cache refresh interval (15 min) is much shorter than KEK rotation period (1 year)
- No special handling needed - background task continues refreshing normally
- After KEK rotation, next refresh (within 15 min) will use new KEK version
6.2 Payment network key rotation (Arkéa-provided keys)
Frequency:
- ZMK: retained according to the Arkéa-approved wrapping-key lifecycle. Arkéa confirmed that AES-256 ZMK material has a longer cryptoperiod than TDES issuing keys; Green-Got retains the current production ZMK through the end of 2030 to cover the next payment-key renewal.
- TDES TR-31 issuing/payment keys: 3-year issuance cryptoperiod from activation, with a 2-3 month operational overlap during replacement.
- Authorization/validation tail: the previous TDES key set remains available only for cards already issued under that key set until those cards expire. The total lifetime is capped at 8 years, covering a 3-year issuance period plus up to 5 years of card validity.
Process:
- Arkéa draws the replacement TDES TR-31 key set before the end of the current issuance period.
- Arkéa distributes replacement key material to Green-Got through the secure key ceremony and partner channels.
- Green-Got imports the replacement ZMK or wrapped TR-31 key blocks into AWS Payment Cryptography.
- Green-Got validates KCVs, AWS Payment Cryptography key state, enabled key modes, and partner activation dates.
- Green-Got updates key ARN references so new card issuance and personalization use the replacement key set after partner rollout.
- During the 2-3 month overlap, both the previous and replacement key sets remain available for controlled validation and cutover.
- 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.
- 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:
- Arkéa generates new KAAV key (HMAC-256)
- Arkéa distributes to both Green-Got and Apata simultaneously (wrapped under ZMK)
- Green-Got imports new key to AWS Payment Cryptography
- Apata updates key in their system
- Grace period where both old and new keys are valid for validation
- 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:
- Green-Got confirms the active Mastercard PKI validity window with Arkéa or Exceet before the next issuing-key renewal.
- Green-Got records the applicable PKI expiry date in the key and certificate inventory.
- Arkéa, Mastercard, and Exceet operate the personalization PKI and related certificate material.
- 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.
- 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 Type | Storage | Access Control | Exportability | Rotation |
|---|---|---|---|---|
| KEK (KMS) | AWS KMS HSM | IAM policies, MFA required | Never exported | Annual (automatic) |
| Data Keys (cached) | In-memory cache (15 min), encrypted by KEK in DB | IAM policies | Plaintext zeroized on refresh | Every 15 minutes (automatic) |
| Data Keys (stored) | Encrypted by KEK, stored with each ciphertext | IAM policies | Decrypted only for that ciphertext | N/A (tied to specific data) |
| ZMK (AES-256 or TDES) | AWS Payment Cryptography HSM | IAM policies, MFA required | Never exported | Current production ZMK retained through end-2030 per Arkéa lifecycle |
| PVK_VISA (TDES) | AWS Payment Cryptography HSM | IAM policies | Never exported | 3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total |
| PEK_MC_AUTH (TDES) | AWS Payment Cryptography HSM | IAM policies | Never exported | 3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total |
| PEK_EXCEET (TDES) | AWS Payment Cryptography HSM | IAM policies | Never exported | 3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total |
| KCVX (TDES) | AWS Payment Cryptography HSM | IAM policies | Never exported | 3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total |
| IMK Set (TDES) | AWS Payment Cryptography HSM | IAM policies | Never exported | 3-year issuance cryptoperiod; retained for non-expired cards, up to 8 years total |
| KAAV Key (HMAC-256, TR-31 M7) | AWS Payment Cryptography HSM | IAM policies, MFA required | Never exported | Per 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:
- Cached plaintext data key: Stored in memory using
Zeroizing<Vec<u8>>wrapper - Automatic zeroization: Old keys zeroized when cache refreshes (every 15 minutes)
- Encrypted data key: Also zeroized (defense in depth, though not strictly required as it’s encrypted)
- Lock-free reads:
ArcSwapprovides thread-safe access without lock contention - In-flight safety: During refresh, in-flight operations continue using old key until completion (no interruption)
Memory security for cardholder data:
- Type-level protection:
Encrypted<T>wrapper ensures data is stored as ciphertext by default - Lazy decryption: Data remains encrypted until explicitly accessed via
.expose()or.into_inner() - Automatic zeroization: If
TimplementsZeroize,Encrypted<T>auto-implementsZeroizeOnDrop - Display/Debug protection:
Encrypted<T>displays as****to prevent accidental logging - Explicit API: Developers must explicitly call async methods to access plaintext (prevents accidental exposure)
Allowed scenarios for plaintext CHD/SAD:
- Masking for display: Decrypt PAN from KMS, extract first 6 + last 4 digits, zeroize clear PAN
- Partner communication: Decrypt from KMS, re-encrypt via appropriate HSM (e.g., TDES for Exceet), transmit, zeroize
- Authorization validation: Decrypt from KMS only for issuer-side reference data when needed, validate transaction (ARQC, PIN reference data, AAV), zeroize
- Database write: Encrypt using cached data key (synchronous), write envelope to DB, zeroize plaintext
Prohibited scenarios:
- Logging clear CHD/SAD (even in debug logs)
- Passing clear CHD/SAD to non-CDE services
- Storing clear CHD/SAD in temporary files or caches
- Transmitting clear CHD/SAD over unencrypted channels
Enforcement:
Encrypted<T>type prints****by default- All logging implementations redact sensitive fields
- Code reviews enforce clear data handling policies
- Static analysis tools flag suspicious patterns
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:
- KMS only authenticates the wrapped data key (not the payload)
- ChaCha20-Poly1305 has no AAD binding the ciphertext to specific context
Mitigation in current design:
- Application-level access controls prevent unauthorized database modifications
- Database audit logs track all changes
- Each encryption uses a unique nonce, preventing direct ciphertext comparison
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
| Requirement | Implementation |
|---|---|
| Encrypt CHD at rest | ChaCha20-Poly1305 with KEK+data key envelope encryption for all stored card data and issuer-side PIN reference data |
| Protect keys | KEK in KMS HSM (never exported), payment keys in AWS Payment Cryptography HSM |
| Rotate keys | KEK 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 controls | IAM policies with least privilege, MFA for HSM access, quarterly reviews |
| Audit logging | CloudTrail logs all KMS and AWS Payment Cryptography API calls, immutable S3 storage |
| Network segmentation | CDE components in private subnets, restrictive security groups, VPC endpoints for HSMs |
| No clear PIN storage required | The 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:
- Always use
Encrypted<T>wrapper for CHD/SAD fields in database models - Encryption is synchronous: Uses cached KMS data keys (no
awaitneeded forto_encrypted_blob()) - Decryption is async: Requires KMS call via
.expose()or.into_inner()(mustawait) - Use AWS Payment Cryptography for PIN-domain and partner operations only; keep storage encryption in KMS
- Zeroize clear CHD/SAD immediately after use (automatic with
Encrypted<T>if inner type implementsZeroize) - Never log clear CHD/SAD (
Encrypted<T>displays as****automatically) - Test with synthetic data (never use real PANs/PINs in dev/staging)
Application startup:
- Required: Call
aws_kms::start_background_refresh()during application initialization - Before encryption: Application must wait for first data key generation (cache has
.wait_ready()method) - Failure mode: Without cache initialization, encryption operations will panic with “KMS data key cache not initialized”
8.2 For operations
Monitoring:
- KMS data key cache health: Monitor background refresh success/failure logs
- CRITICAL alerts if initial key generation fails for >2 minutes (encryption unavailable)
- CRITICAL alerts if refresh fails for >15 minutes (stale key, security risk)
- HIGH alerts if refresh fails for >8 minutes
- MEDIUM alerts if refresh fails for >2 minutes
- KMS API usage: CloudTrail for audit logging of all encrypt/decrypt calls
- Should see ~4
GenerateDataKeycalls per hour (15-minute refresh) - Should see
Decryptcalls only for data reads (not writes) - Spike in
GenerateDataKey= cache initialization issues
- Should see ~4
- Performance metrics: Encryption operations should be <1ms (synchronous, no network)
- Decryption metrics: Track KMS decrypt latency (expect 20-100ms per call)
Key rotation:
- KEK rotation: Annual (automatic via AWS KMS), monitor for new key versions
- Data key rotation: Automatic every 15 minutes (background task)
- Payment keys: ZMK retained per Arkéa lifecycle; TDES TR-31 issuing/payment keys replaced after a 3-year issuance period with a 2-3 month overlap and authorization retention for non-expired cards
- Card-personalization PKI: Mastercard PKI validity tracked against card expiry; current validity through end-2035 and expiry review planned around 2030
Operational procedures:
- Audit IAM permissions quarterly (who can access KMS, AWS Payment Cryptography)
- Test key rotation in staging before production
- Maintain key inventory (spreadsheet of all keys, ARNs, rotation dates)
- Cache restart: If background task fails, application restart will reinitialize cache
8.3 For compliance
PCI DSS audit focus areas:
- Annual PCI DSS audit (QSA reviews key management, encryption implementations)
- Data key caching: Demonstrate 15-minute rotation meets key management requirements
- Memory security: Show
Zeroizingwrappers and automatic cleanup - Penetration testing of encryption flows (ensure clear data not leaked, verify zeroization)
- Incident response plan for key compromise:
- KEK compromise: Rotate KEK, re-encrypt all data with new KEK
- Data key compromise: Cache auto-rotates within 15 minutes, optionally re-encrypt affected records
- Payment key compromise: Import new keys from Arkéa, coordinate with partners
- Key backup and recovery procedures documented and tested
- Quarterly vulnerability scans of CDE components
9. Related documentation
For specific data element details, see:
- PIN Operations - PIN lifecycle, encryption, verification
- PAN Handling - PAN generation, storage, masking, and keyed HMAC lookup values
Change History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-02-10 | julian@green-got.com | Create the card-domain cryptography and key-management reference document. |
| 1.1 | 2026-04-02 | julian@green-got.com | Add the cardholder data flow details and supporting architecture clarifications. |
| 1.2 | 2026-04-17 | julian@green-got.com | Align PIN architecture, partner communication flows, and payment cryptographic operations. |
| 1.3 | 2026-04-21 | julian@green-got.com | Clarify AWS KMS data key wording and storage-encryption terminology. |
| 1.4 | 2026-04-30 | julian@green-got.com | Align PAN lookup HMAC terminology and lifecycle details with the current application-managed design. |
| 1.5 | 2026-06-04 | julian@green-got.com | Document 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).
Card
0. Documentation Index
Card Documentation Index
Foundation Documents
- 1. Cardholder Environment - CDE overview, actors, data classification (CHD/SAD), PCI DSS scope
- 2. Cryptography & Key Management - Key architecture, HSM usage, encryption patterns
- 3. Cardholder Data Flow - Complete CHD flow diagram (all channels: EMV, e-commerce, MDES, magstripe, ATM), ingress/egress points, storage, partners (PCI DSS compliance)
Card Components
- 3. PIN - PIN structure, storage, verification (offline/online), lifecycle
- 4. PAN - PAN lifecycle, generation, pool management, encryption, HMAC lookup, reuse controls
Authentication Flows
- 6. 3DS - Authentication for 3DS challenges
Transaction Flows
- 8. EMV Transactions - Chip contact and contactless (NFC), ARQC, DE-55, CVM, ATC, ARPC
Storage
- 14. Raw Authorization Messages Storage - Storage of raw ISO 8583 messages in ClickHouse, encryption, access controls
- 15. Clearing Files Processing - Processing of clearing files received from Mastercard, including S3 storage, encryption, and access controls
- 16. ClickHouse Synchronization - Synchronization of PostgreSQL data to ClickHouse, including encrypted SAD/CHD, access controls
Others
- 17. Automatic Billing Updater (ABU) - Mastercard service for updating cardholder information for recurring payments
To Be Created
-
- CVC - CVC generation, validation, storage
-
- MDES - Mastercard Digital Enablement Service, tokenization (referenced in 3_cardholder_data_flow.md)
-
- E-commerce - Card-not-present transactions, CNP flows (overview in 3_cardholder_data_flow.md)
-
- MDES Transaction - Mobile wallet transaction flows (overview in 3_cardholder_data_flow.md)
-
- ATM - ATM withdrawal flows (overview in 3_cardholder_data_flow.md)
-
- Card Issuance - Card ordering, renewal, replacement, lifecycle
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:
- Archival: The file is encrypted in CBS memory using ChaCha20-Poly1305 with a KMS-generated 256-bit data key (AWS KMS envelope encryption) and stored in a dedicated S3 bucket accessible with IAM least-privilege roles. This copy is retained in case a processing retry is needed, and for debugging or investigation purposes.
- Processing: The file is processed in the CBS memory in cleartext. CHD and SAD present in the file (including PAN and track data equivalents) exist transiently in memory during processing only. Once processing completes, the original file is deleted and memory is zeroized. If processing fails, the file is retained in the S3 bucket for retry and investigation purposes.
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:
- Old Account Number (PAN)
- Old Expiration Date
- New Account Number (PAN)
- New Expiration Date No SAD is transmitted.
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:
- Encipherment: PIN block protection using payment PIN keys (PEK) inside AWS Payment Cryptography.
- Encryption: Generic data protection such as KMS envelope encryption for issuer-side storage.
- PTC (PIN Try Counter): chip-side counter maintained by the EMV chip for offline PIN attempts.
- Reveal PIN: On-demand PIN display to the cardholder through the dedicated PIN iframe. This is backed by an issuer-side encrypted PIN block stored for this purpose.
Key architecture overview:
Issuer-owned persistent data:
- Online PIN reference data:
pin_pvv(Visa PVV) encrypted at rest in PostgreSQL with KMS. This is the issuer-side source of truth for online PIN verification. - Reversible PIN artifact:
encrypted_pin_blockencrypted at rest in PostgreSQL with KMS. This issuer-side artifact is retained underPEK_MC_AUTHto support reveal PIN, Exceet transmission (viaPEK_EXCEET), and chip PIN change preparation. - Attempt tracking: a Green-Got online PIN attempt counter and a mirrored chip-side PTC. Each starts at
3and has a distinct block status. - No clear PIN storage in the target design: the clear PIN is not stored in PostgreSQL.
AWS Payment Cryptography keys and functions:
- PVK_VISA: issuer-only PIN Verification Key used to generate and verify Visa PVV data.
- PEK_MC_AUTH: dedicated PEK for PIN blocks transported in authorization and ATM PIN management flows through Mastercard.
- PEK_EXCEET: dedicated PEK for manufacturer communication and chip personalization payloads sent to Exceet.
- IMK-SMI / IMK-SMC: issuer master keys used to generate EMV issuer scripts for offline PIN change on chip.
- ECDH key agreement key: asymmetric key used by CBS and AWS Payment Cryptography for reveal PIN transport protection.
Design decision:
- The chosen baseline is AWS Payment Cryptography with Visa PVV + issuer-side encrypted PIN block because it fits the current product constraints:
- user-selected PIN at card order time,
- online PIN as the issuer source of truth,
- reveal PIN support,
- chip PIN update through issuer script after an app-driven PIN change.
- The chip PIN is synchronized asynchronously after a PIN change. During that window, the online PIN is already updated while the offline chip PIN remains unchanged until an online chip transaction applies the issuer script.
- Magstripe is treated as a legacy fallback only. It is not the issuer source of truth for PIN verification.
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:
- Length: exactly 4 digits
- Format: numeric only (
0-9) - Entropy: 10,000 possible combinations
- Origination: for physical cards, the cardholder chooses the PIN in the mobile app at card order time
- Secret handling: the PIN is never logged and is never stored in clear text
PIN origination model:
- Physical card order includes a mandatory PIN selection step in the app.
- The app opens a dedicated PIN iframe for PIN entry.
- The dedicated iframe sends the selected PIN to CBS over TLS on the authenticated app session.
- CBS enforces business validation rules and prepares the AWS-required encrypted PIN block before generating the PVV.
- AWS Payment Cryptography derives and returns:
- a Visa PVV for online verification,
- an issuer-side encrypted PIN block for reveal PIN and later PIN lifecycle operations.
- CBS stores the encrypted PVV, the encrypted PIN block, initializes the Green-Got online PIN attempt counter to
3, and stores the mirrored chip PTC as3. - When Exceet needs a personalization PIN block, AWS Payment Cryptography translates the stored issuer PIN block under the manufacturer PEK.
Clear PIN exposure model:
- The cardholder knows the PIN and enters it into the app, ATM, or payment terminal.
- CBS handles issuer-side PIN flows through encrypted PIN blocks and derived reference data.
- Payment keys such as PEK and PVK remain HSM-resident and non-exportable in the target design.
- The target design does not depend on keeping payment keys in application memory. That pattern is not part of the intended production design and is not treated as a compliance baseline.
PIN verification methods:
- Offline PIN: standard for chip POS transactions in Europe. The chip verifies the PIN locally.
- Online PIN: standard for ATMs. The issuer verifies the PIN by comparing the inbound encrypted PIN block against the stored Visa PVV inside AWS Payment Cryptography.
Purpose:
- Authenticate the cardholder at ATMs
- Authorize chip-and-PIN transactions at POS
- Support PIN reveal in the app
- Support PIN unlock and PIN change flows for physical cards
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
| Boundary | Key / protection | PIN block format | Status | Usage |
|---|---|---|---|---|
| Dedicated PIN iframe to CBS | TLS 1.2+ app session | Clear PIN field, not a PIN block | Defined | PIN selection and PIN change form submission |
| CBS to AWS Payment Cryptography | ECDH-derived protection | ISO-4 | Defined by AWS Payment Cryptography ECDH flows | Input to TranslatePinData before PVV generation |
| Mastercard authorization / ATM PIN management | PEK_MC_AUTH | To be confirmed with Mastercard / processor | Undefined | Online PIN authorization, ATM PIN management, stored issuer-side encrypted PIN block |
| Exceet manufacturing | PEK_EXCEET (TR-31 P0, TDES) | ISO-0 | Defined | Manufacturer PIN block included in the card personalization payload |
| EMV chip PIN change script | IMK-SMI / IMK-SMC secure messaging | Scheme-defined APDU payload | Defined by EMV profile and issuer script support | Deferred offline PIN synchronization on chip |
Design rule:
PEK_MC_AUTHis the issuer-side canonical PIN block boundary because it supports transaction operations.PEK_EXCEETis a manufacturing-only boundary.- AWS Payment Cryptography performs format and key translation inside the HSM boundary.
- Exceet receives ISO-0 under
PEK_EXCEET; Mastercard format remains undefined until confirmed.
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:
- Store Visa PVV as the issuer-side reference for online PIN verification.
- Store an issuer-side encrypted PIN block as the reversible artifact retained under
PEK_MC_AUTHand translated on demand for reveal PIN and Exceet transmission. - Encrypt both values at rest with KMS like other SAD retained by the issuer under PCI DSS 4.0.1 Requirement
3.3.3.
Why this is the selected baseline:
- The product requires:
- PIN selection at card creation,
- online PIN as the issuer source of truth,
- reveal PIN in the app,
- later chip synchronization through issuer scripts.
- Visa PVV is the selected algorithm for backend verification.
- The retained encrypted PIN block complements PVV by preserving the reversible issuer-side artifact needed for reveal PIN, Exceet transmission, and EMV PIN change preparation.
Persisted issuer-side data:
- PIN verification value for online issuer-side verification, encrypted at rest with ChaCha20-Poly1305 using issuer-managed data keys protected by AWS KMS
- Issuer-side PIN block retained under
PEK_MC_AUTHby AWS Payment Cryptography, then encrypted at rest with ChaCha20-Poly1305 using issuer-managed data keys protected by AWS KMS; translated on demand for reveal PIN and Exceet transmission - Green-Got online PIN remaining attempts, initialized to
3 - Mirrored chip-side PTC (PIN Try Counter), initialized to
3and updated from EMV data when available - PIN synchronization status for the chip, either
SynchronizedorPendingChipSync - Online PIN block status, either
OperationalorOnlinePinBlocked - Chip PIN block status, either
OperationalorChipPinBlocked - Timestamp recorded when either block status is reached
Key points:
- No clear PIN storage: the target design does not store the PIN in PostgreSQL.
- Reveal PIN remains possible: AWS translates the stored encrypted PIN block into a CBS-controlled reveal response rendered only inside the dedicated iframe used for PIN display.
- Manufacturer distribution remains possible: AWS translates the stored encrypted PIN block under
PEK_EXCEET. - Verification remains HSM-only: online PIN verification compares the inbound encrypted PIN block against stored PVV inside AWS Payment Cryptography.
- Payment keys stay HSM-only:
PVK_VISA,PEK_MC_AUTH,PEK_EXCEET, and IMKs are not application-side storage keys.
Dedicated iframe usage:
- Set PIN and change PIN: the dedicated app iframe handles PIN entry and sends the selected PIN to CBS over TLS on the authenticated app session. CBS builds an ISO-4 PIN block protected by an ECDH-derived key, translates it under
PEK_MC_AUTHusing the Mastercard-required PIN block format, and then callsGeneratePinData. - Reveal PIN: CBS performs the AWS Payment Cryptography translation, recovers the clear PIN in controlled server memory, and renders the dedicated iframe response containing the clear PIN.
- Security goal: the dedicated iframe isolates PIN UI handling and network access while payment keys remain inside AWS Payment Cryptography.
3.2 PIN Verification
Green-Got supports two verification methods. The terminal and chip decide which one applies for a given transaction.
| Method | Frequency | Who verifies | Green-Got involvement | Main use case |
|---|---|---|---|---|
| Offline PIN | Standard for POS | EMV chip | Validates ARQC and mirrors chip PTC when available | POS transactions in Europe |
| Online PIN | Standard for ATM | Green-Got via AWS Payment Cryptography | Receives encrypted PIN block and verifies against stored PVV | ATM 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:
- does not receive the PIN,
- does not receive a PIN block,
- validates ARQC,
- mirrors chip-side remaining attempts based on DE-55 when present.
Technical details:
- No DE-52 PIN block is expected in the standard offline flow.
- EMV data in DE-55 carries the relevant information:
- Tag 9F34: CVM Results
- Tag 9F17: PIN Try Counter
- TVR: terminal verification status flags
- The chip-side PTC starts at
3and is authoritative for offline PIN attempts. - Green-Got stores a mirrored chip PTC when the value is available in DE-55.
- When the mirrored PTC reaches
0, Green-Got records a chip PIN block status distinct from the online PIN block status.
Advantages:
- No network round-trip for verification
- PIN never leaves the chip in the standard POS flow
- Standard European EMV behavior
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:
- PVK_VISA for Visa PVV verification logic
- PEK_MC_AUTH for the inbound PIN block transported by Mastercard
When used:
- ATM cash withdrawals
- ATM balance inquiries
- ATM mini statements
- Deferred chip synchronization after app PIN change
- Some fallback or international online PIN scenarios
Verification during transaction:
- Terminal or ATM sends an encrypted PIN block.
- Mastercard translates that PIN block to the issuer’s authorization PEK.
- CBS fetches the stored encrypted PVV and decrypts it with KMS.
- CBS calls
VerifyPinDatawith:- the encrypted PIN block,
- PAN,
- Visa PIN verification attributes,
PVK_VISA,PEK_MC_AUTH.
- AWS Payment Cryptography:
- decrypts the inbound PIN block,
- derives the Visa PVV candidate,
- compares the result with the stored PVV.
Critical note:
- CBS does not decrypt the inbound PIN block.
- Online PIN verification happens entirely inside AWS Payment Cryptography.
AWS references:
GeneratePinData: AWS Payment Cryptography APIVerifyPinData: AWS Payment Cryptography APIPinGenerationAttributes: AWS Payment Cryptography APIPinVerificationAttributes: AWS Payment Cryptography API
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:
- Network round-trip required
- Encrypted PIN block travels through the network
- Issuer directly maintains and resets the Green-Got online PIN attempt counter
- Online PIN failures do not decrement the chip-side PTC
- Standard for ATMs
3.2.3 Magstripe Transactions (International Compatibility)
Context: Magstripe support exists for international compatibility and legacy acceptance.
Supported CVM (Cardholder Verification Method):
| CVM method | Support | Use case | PIN verification |
|---|---|---|---|
| Signature | Yes | Legacy US retail and fuel flows | No PIN |
| Online PIN | Yes | ATM and some fallback flows | Same Visa PVV verification as ATM |
Notes:
- Track data remains ephemeral and is not treated as the issuer PIN source of truth.
- Magstripe online PIN, when accepted, uses the same issuer-side verification model as ATM:
- inbound PIN block,
- translation to
PEK_MC_AUTH, VerifyPinDataagainst stored PVV.
- Offline magstripe transactions are not supported in real time. They are handled through the normal clearing process and are outside the live PIN verification model.
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:
- ATM online PIN verification
- ATM PIN change request handling
- Any issuer-side online PIN flow that arrives through Mastercard
Key property:
- This PEK is distinct from the manufacturer PEK.
- It is not the key shared with Exceet for personalization.
Rotation note:
- Stored issuer PIN blocks remain dependent on the
PEK_MC_AUTHkey material used when they were generated or last translated. - PEK rotation therefore includes continuity for existing PIN blocks: either previous PEK material remains available until all dependent issuer PIN blocks have been replaced, or stored issuer PIN blocks are re-enciphered under the replacement PEK before retiring the previous key.
- Retiring the previous
PEK_MC_AUTHwithout preserving this continuity breaks reveal PIN, Exceet translation, and EMV PIN change preparation for cards whose stored issuer PIN block still depends on the retired key.
3.3.2 Exceet Manufacturing PEK
Purpose: protect PIN blocks sent to Exceet for manufacturing and personalization.
Used for:
- card manufacturing payloads,
- chip personalization PIN data,
- any outbound PIN block specifically prepared for Exceet.
Key property:
- This key is only for the manufacturing boundary.
- It is not the key used in Mastercard authorization transport.
3.3.3 Why the Split Matters
- Different counterparties hold the keys.
- Rotation cadence can differ.
- A compromise in one transport domain does not automatically affect the other.
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:
- Exactly 4 digits
- No ascending sequences such as
1234,2345 - No descending sequences such as
4321,9876 - No repeated digit patterns such as
1111,0000 - No match with the cardholder’s date of birth permutations when DOB is known:
ddmmmmddmmyyyymmddyyyydd
- Validation is enforced server-side even if the app also performs local UX checks
Storage result:
- Store the PIN verification value for online issuer-side verification
- Store the issuer-side encrypted PIN block retained under
PEK_MC_AUTH - Initialize Green-Got online PIN attempts to
3 - Initialize mirrored chip PTC to
3 - Mark chip PIN synchronization status as
Synchronized - Keep the online PIN block status and chip PIN block status as
Operational
Why this design is preferred:
- The issuer does not persist the clear PIN itself.
- Exceet receives only the manufacturer PIN block it needs.
- Later reveal PIN is still possible through AWS Payment Cryptography and the dedicated server-rendered iframe.
- Later online PIN verification still works through AWS Payment Cryptography.
4.2 PIN Verification (Transaction Authorization)
PIN verification follows one of two paths:
-
Offline PIN verification
- Standard for POS in Europe
- Chip verifies the PIN
- Green-Got validates ARQC and mirrors chip PTC when available
-
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:
- The Green-Got online PIN counter and chip-side PTC both start at
3. - Offline PIN: the chip PTC is authoritative for chip verification and is mirrored by Green-Got when the value is available in DE-55.
- Online PIN: Green-Got decrements only the online PIN counter.
- Online PIN failures do not decrement chip PTC. A card with two failed online PIN attempts still has the full chip-side PTC for later offline PIN attempts unless the chip has independently decremented it.
- The effective issuer decision uses the minimum available value between the Green-Got online PIN counter and the mirrored chip PTC.
- Green-Got records distinct block statuses for online PIN exhaustion and chip PTC exhaustion.
Method selection:
- POS terminals default to offline PIN
- ATMs default to online PIN
- Green-Got does not override the CVM selected by terminal and chip behavior
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:
- the cardholder chooses the new PIN in the app,
- Green-Got applies strong app authentication before accepting the request,
- Green-Got validates the same business rules as at card order time,
- the issuer source of truth is updated immediately,
- the chip is synchronized later during a compatible online chip transaction, typically an ATM session.
This keeps the anti-pattern validation logic consistent and makes the transition state explicit in the application. During the PendingChipSync period:
- online PIN = new PIN,
- offline chip PIN = old PIN,
- the app must clearly instruct the cardholder to visit an ATM to finish chip synchronization.
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:
- Same validation rules as the initial order flow
- Strong authentication is required before the app accepts the change request
- No requirement to store the clear PIN itself
- Online PIN is updated immediately after the app flow succeeds
- Successful online PIN verification resets the Green-Got online attempts independently of chip synchronization
- Chip PTC is reset in Green-Got only after the chip synchronization script succeeds
- Chip synchronization happens later and is explicitly tracked in application state
- Offline chip update uses
GenerateMacEmvPinChangeplus IMK-SMI and IMK-SMC - The manufacturer does not need the issuer PVK for later PIN change
AWS references:
GenerateMacEmvPinChange: AWS Payment Cryptography API- EMV PIN change user guide: AWS Payment Cryptography user guide
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.
- If only the Green-Got online PIN counter is exhausted, Green-Got resets the online PIN counter after app authentication.
- If the chip PTC is exhausted, chip-side unblock is a separate scheme-defined EMV issuer script flow. It is separate from the Green-Got online-counter unblock flow and is not handled by
GenerateMacEmvPinChange. - The PIN itself does not need to change.
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:
- Strong app authentication identifies the cardholder before the unlock intent is registered.
- Physical card possession is proven later at the ATM before the PIN unblock issuer script is issued.
- No PIN change required
- Online PIN unblock does not reset the chip-side PTC
- Chip PIN unblock is tracked separately from the Green-Got online-counter unblock flow
GenerateMacEmvPinChangeis used for offline PIN change, not chip PIN unblock
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:
- Reveal PIN is supported for physical cards.
- The app requires a dedicated step-up authentication before opening the mobile app section that hosts the reveal PIN iframe.
- The dedicated iframe presents an explicit user confirmation and disclaimer before display.
- CBS performs the AWS Payment Cryptography reveal operation and renders the clear PIN only inside the dedicated iframe response.
- The dedicated iframe is the only app component authorized to display the clear PIN.
- Reveal PIN does not change the online PIN, the chip PIN, or the attempt counters.
5. Used In
PIN authentication is used in the following flows:
-
EMV chip transactions -> 8_emv_transactions.md
- Standard POS behavior in Europe is offline PIN
- Green-Got validates ARQC and mirrors chip PTC when available
- Rare online PIN fallback remains possible
-
ATM withdrawals and ATM services
- Standard behavior is online PIN
- Green-Got verifies the encrypted PIN block against stored Visa PVV
- Green-Got online PIN attempts start at
3and reset to3after a successful online verification
-
Physical card order
- The cardholder chooses the PIN in the app
- The issuer validates anti-pattern rules before the card enters manufacturing
-
PIN reveal in the app
- Supported through a dedicated server-rendered iframe flow
- Uses the stored issuer encrypted PIN block
- Requested and displayed only through the dedicated iframe authorized to handle clear PIN display
- Requires step-up authentication before opening the reveal section, then explicit user confirmation and disclaimer before display
PIN is not used in:
- e-commerce transactions
- MDES wallet transactions
- low-value contactless transactions with no CVM
6. Security Considerations
6.1 PCI DSS Requirements
| Requirement | Implementation |
|---|---|
| PIN never in clear text in issuer storage | The target design stores encrypted Visa PVV and an encrypted issuer PIN block, not the clear PIN itself |
| Protected during transport | Dedicated iframe over TLS for app set/change and reveal display; dedicated PEKs for network and manufacturer PIN blocks |
| Encrypted at rest | pin_pvv, encrypted_pin_block, and card security state encrypted at rest with KMS |
| Strong key separation | PVK_VISA, PEK_MC_AUTH, PEK_EXCEET, and IMKs remain separated by use case |
| Verification without exposure | Online PIN verification occurs entirely inside AWS Payment Cryptography |
| No exported payment keys | PEKs, PVKs, and IMKs remain HSM-resident in the target design |
| Auditability | CloudTrail and issuer logs capture HSM calls without exposing PIN material |
6.2 Attack Mitigations
- Three-attempt model
- Green-Got online PIN attempts start at
3 - chip-side PTC starts at
3 - Green-Got stores both values and uses the minimum available value for issuer-side blocking decisions
- online PIN exhaustion and chip PTC exhaustion produce distinct block statuses
- Green-Got online PIN attempts start at
- Different PEKs for different trust boundaries
PEK_MC_AUTHfor online transportPEK_EXCEETfor manufacturing
- No clear PIN storage
- avoids storing the clear PIN in issuer databases while still supporting reveal PIN
- HSM-only verification
- inbound PIN block is never decrypted in CBS
- PAN binding
- PAN-bound PIN block formats stay tied to the correct PAN
7. Related Concepts
7.1 Comparison with Other Authentication Values
| Term | Description | Algorithm / key family | Use case |
|---|---|---|---|
| PIN | 4-digit cardholder secret | Cardholder knowledge factor | Cardholder authentication |
| PVV | Visa PIN Verification Value | TR31_V2_VISA_PIN_VERIFICATION_KEY | Issuer-side online PIN verification baseline |
| Issuer Encrypted PIN Block | Reversible issuer-side PIN artifact | PEK_MC_AUTH (TR-31 P0) | Reveal PIN, EMV PIN change preparation, manufacturer translation |
| PIN Block | Protected PIN data in transit | PEK_MC_AUTH for Mastercard-required format; PEK_EXCEET (TR-31 P0) for Exceet ISO-0 | Online transport or manufacturing distribution |
| ARQC | EMV application request cryptogram | IMKac-derived | Chip transaction validation |
| AAV | 3DS authentication value | KAAV (TR-31 M7) / HMAC-256 | E-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:
- The BIN used to generate the PAN
- A scope, such as standard card PANs, ephemeral card PANs, or test PANs
- A lifecycle status
- An availability timestamp used for assignment ordering
- A keyed HMAC-SHA256 lookup value used for lookup without decrypting the PAN
- The encrypted PAN value
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:
- BIN: 8 digits
- Account number: 7 digits
- Check digit: 1 digit, calculated with the Luhn algorithm
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:
- Select the BIN.
- Generate every 7-digit account-number value from
0000000to9999999. - Calculate the Luhn check digit for each BIN and account-number pair.
- Build the full 16-digit PAN.
- Randomly shuffle the generated PAN list before insertion so issued PANs are not sequential.
- Generate a keyed HMAC-SHA256 lookup value for each PAN.
- Encrypt the PAN before storage.
- 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:
| Scope | Purpose |
|---|---|
| Standard | PANs used for ordinary physical and virtual card issuance. |
| Ephemeral | PANs reserved for short-lived or one-time card products. |
| Test | PANs 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:
pan_hmac1withpan_hmac1_updated_atpan_hmac2withpan_hmac2_updated_at
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:
- the active PAN lookup HMAC key
- the secondary rotation key
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:
- AWS KMS protects the key-encryption key.
- AWS KMS generates 256-bit data keys.
- The application encrypts PAN values with ChaCha20-Poly1305 using the data key.
- The encrypted data key is stored with the ciphertext.
- Plaintext PAN and plaintext data key material exist only transiently in application memory and are zeroized after use.
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:
| Status | Meaning |
|---|---|
| Reserved for test | PAN is reserved for test or certification usage and is never assigned to a live customer card. |
| Available | PAN is assignable to a new card after its available_at timestamp has passed. |
| Assigned | PAN is referenced by an active card or a card in issuance. |
| Locked | PAN is not assignable after card expiry, cancellation, replacement, or closure. |
| Retired | PAN 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:
| Scope | Lock period |
|---|---|
| Standard | Two years after card expiry, replacement, cancellation, or closure. |
| Ephemeral | Shorter product-specific lock period, validated against clearing, settlement, dispute, fraud-monitoring, and partner-processing windows before production use. |
| Test | Not 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:
- Total generated PANs
- Available PANs
- Assigned PANs
- Locked PANs
- Retired PANs
- Test-reserved PANs
- Ephemeral PANs
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:
- PANs are not stored in cleartext.
- PANs are not issued sequentially.
- HMAC lookup avoids decrypting PANs for matching.
- Test PANs are segregated from live customer PANs.
- Ephemeral PANs use a dedicated scope and a separately defined lock period.
- Logs and APIs expose only tokenized identifiers or masked PAN values.
- Reuse is delayed by the applicable lock period and blocked permanently for compromised or high-risk PANs.
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:
- whether the card exists
- is enabled
- the cardholder’s language preference
- institution financial ID
- the Green-Got card identifier as
externalId - other data required by Apata to drive the challenge
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:
- ARQC: Application Request Cryptogram - Cryptographic signature proving card authenticity
- ARPC: Authorization Response Cryptogram - Cryptographic response from issuer
- ATC: Application Transaction Counter - Anti-replay counter maintained by chip
- CVM: Cardholder Verification Method - Method to verify cardholder (PIN, signature, none)
- DE-55: Data Element 55 - ICC Related Data containing EMV tags
- IMKac: Issuer Master Key for Application Cryptogram - Master key for ARQC/ARPC
- PAN: Primary Account Number - Card number
- PVV: PIN Verification Value - Used for issuer-side online PIN verification
- PTC: PIN Try Counter - Chip-side offline PIN attempt counter
- TVR: Terminal Verification Results - Terminal verification flags
- CHD: Cardholder Data - PAN, cardholder name, expiry date
- SAD: Sensitive Authentication Data - ARQC, PIN blocks, track data (never stored post-auth)
1. Introduction
Green-Got cards contain an EMV chip that supports two transaction interfaces:
- Chip Contact: Physical insertion into a card reader with electrical connectors (ISO 7816)
- 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):
- Single transaction: €50 without PIN
- Consecutive transactions: 5 transactions without PIN
- Cumulative amount: €150 without PIN
When limits exceeded:
- Terminal prompts: “Please insert card”
- Transaction requires chip contact with PIN
- Limits reset after successful chip contact + PIN transaction
Note: Green-Got also enforces daily/monthly spending limits and maximum transaction amounts configured per card.
3. Transaction Comparison
| Aspect | Chip Contact | Contactless (< €50) | Contactless + PIN (≥ €50) |
|---|---|---|---|
| Interface | Physical connectors | NFC wireless | NFC wireless + PIN |
| Insertion | Full insertion | Tap only | Tap only |
| PIN required | Yes (standard) | No | Yes |
| Mastercard limit | No hard limit | €50 | No hard limit |
| Consecutive limit | None | 5 transactions | Resets counter |
| Cumulative limit | None | €150 | Resets cumulative |
| ARQC generation | ✅ Yes | ✅ Yes | ✅ Yes |
| Cryptographic security | Full | Full | Full |
| CVM | Offline PIN | No CVM | Offline PIN* |
| Common use | ATM, high-value | Retail, transport | Modern retail terminals |
| Counter reset | Yes | No | Yes |
*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:
- Card can be removed immediately after tap
- No PIN required (CVM = “No CVM performed”)
- ARQC proves chip authenticity and transaction integrity
- Transaction completes in <1 second
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:
- Offline PIN verification: Chip verifies PIN locally, no network call
- No PIN block sent: Green-Got never receives the PIN
- ARQC proves PIN verification: Cryptogram includes PIN verification result
- CVM Results (Tag 9F34): Shows “Offline PIN” was used and if successful
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:
- Requires EMV Kernel 3 or equivalent terminal support
- Resets contactless consecutive/cumulative counters
- Same security as chip contact with PIN
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:
- Card authenticity: Only a genuine chip with the correct IMKac key can generate valid ARQC
- Transaction integrity: Transaction data (amount, date, merchant) is cryptographically bound to ARQC
- 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):
- Extract ARQC, ATC, transaction data from DE-55
- Call AWS Payment Cryptography:
VerifyAuthRequestCryptogram(ARQC, TransactionData, IMKac, PAN, ATC) - AWS derives session key, calculates expected ARQC, compares with received ARQC
- Returns HTTP 200 if valid, HTTP 4xx if invalid
5.2 Key EMV Tags in Authorization Request
Required in Authorization Request/0100:
| Tag | Name | Length | Description |
|---|---|---|---|
| 9F26 | Application Cryptogram (AC) | 8 bytes | ARQC - Authentication cryptogram proving card authenticity |
| 9F27 | Cryptogram Information Data | 1 byte | Cryptogram type (ARQC/TC/AAC) |
| 9F36 | Application Transaction Counter (ATC) | 2 bytes | Anti-replay counter, increments each transaction |
| 9F34 | CVM Results | 3 bytes | MANDATORY - Cardholder Verification Method result |
| 95 | Terminal Verification Results (TVR) | 5 bytes | Terminal verification flags (PIN, offline data auth, etc.) |
| 9F37 | Unpredictable Number | 4 bytes | Terminal random number (prevents pre-computation) |
| 9F10 | Issuer Application Data (IAD) | 1-32 bytes | Issuer-specific data (mandatory if chip provides it) |
| 9A | Transaction Date | 3 bytes | YYMMDD format |
| 9C | Transaction Type | 1 byte | Purchase (0x00), Cash withdrawal (0x01), Refund (0x20), etc. |
| 9F02 | Amount Authorized | 6 bytes | Transaction amount in minor currency units (e.g., cents) |
Common Optional Tags:
| Tag | Name | Length | Description |
|---|---|---|---|
| 5F2A | Transaction Currency Code | 2 bytes | ISO 4217 currency code (e.g., 0978 = EUR) |
| 82 | Application Interchange Profile (AIP) | 2 bytes | Card capabilities (SDA/DDA/CDA support) |
| 9F1A | Terminal Country Code | 2 bytes | ISO 3166 country code |
5.3 CVM Results (Tag 9F34) - MANDATORY
Format: 3 bytes
- Byte 1: CVM performed
- Byte 2: CVM condition
- Byte 3: CVM result
Common values:
| Scenario | CVM Results (Hex) | Meaning |
|---|---|---|
| Contactless < €50 | 3F 00 00 | No CVM performed |
| Chip contact + PIN (success) | 02 03 00 | Offline PIN plaintext, successful |
| Chip contact + PIN (failed) | 02 03 01 | Offline PIN plaintext, failed |
| Online PIN (success) | 01 03 00 | Online PIN, successful |
| Signature | 1F 03 00 | Signature (paper), verified |
Green-Got validation:
- No CVM (0x3F): Amount must be < €50 (contactless limit)
- Offline PIN (0x02): CVM result byte must indicate success (0x00)
- Online PIN (0x01): Validate PIN block in DE-52 separately
- Unknown: Decline as unsupported CVM
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:
- Green-Got tracks last known ATC for each card
- Received ATC must be greater than last known ATC
- If
received_ATC <= last_known_ATC→ Decline as replay attack - Chip increments ATC after generating ARQC (cannot be reversed)
ATC Storage:
- Database tracks last known ATC per card
- Includes suspicious transaction counter for fraud detection
- Updated after each successful authorization
Edge cases:
- ATC wrap-around: When ATC reaches 0xFFFF, may wrap to 0x0000 (chip-dependent)
- Large backward jumps: Flagged as suspicious
- Out of sync: Manual reset may be required (with approval and audit logging)
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:
- Transaction approved
- Issuer wants to send scripts (commands to chip)
- Card configuration requires ARPC for approved transactions
ARPC proves:
- ✅ Response came from legitimate issuer (only Green-Got has IMKac)
- ✅ Response corresponds to specific transaction (same session key)
- ✅ Approval/decline decision is authentic
Generation:
- Call AWS Payment Cryptography
GenerateMacwith IMKac, transaction data, and ATC - Uses same session key as ARQC (derived from IMKac + PAN + ATC)
- Include ARPC in authorization response (Tag 91 in DE-55)
6. CVM (Cardholder Verification Method)
CVM is the method used to verify the cardholder is the legitimate card owner.
Common CVM methods:
- Offline PIN: PIN verified by chip (standard for chip contact POS)
- Online PIN: PIN verified by issuer via Visa PVV (standard for ATMs)
- No CVM: No verification (contactless < €50)
- Signature: Cardholder signs receipt (legacy, rarely used in Europe)
- CDCVM (Consumer Device CVM): Biometric on mobile wallet (Apple Pay, Google Pay)
CVM by Transaction Type:
| Transaction Type | Typical CVM |
|---|---|
| Contactless < €50 | No CVM |
| Contactless ≥ €50 (modern) | Offline PIN (contactless) |
| Contactless ≥ €50 (fallback) | Offline PIN (chip contact) |
| Chip contact purchase | Offline PIN |
| ATM withdrawal | Online 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 Code | Description | User Message |
|---|---|---|
| 51 | Insufficient funds | Insufficient balance |
| 54 | Card expired | Card expired |
| 57 | Transaction not permitted | Transaction not allowed |
| 61 | Exceeds withdrawal limit | Daily limit exceeded |
| 65 | Exceeds withdrawal frequency | Too many transactions |
| 75 | PIN tries exceeded | PIN blocked (online counter or chip PTC exhausted) |
| 89 | Invalid ARQC | Invalid card data |
| 91 | Issuer unavailable | Service 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 Element | Classification | Transmitted From | Transmitted To | Encryption | Retention |
|---|---|---|---|---|---|
| PAN | CHD | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | Encrypted at rest (ChaCha20-Poly1305 via KMS) |
| Cardholder Name | CHD | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | Encrypted at rest (ChaCha20-Poly1305 via KMS) |
| Expiry Date | CHD | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | Encrypted at rest (ChaCha20-Poly1305 via KMS) |
| ARQC | SAD | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | ❌ Never stored (ephemeral, validated only) |
| ATC | Non-sensitive | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | Last value stored (anti-replay tracking) |
| CVM Results | Non-sensitive | Card chip → Terminal | Terminal → Mastercard → Green-Got | TLS 1.3 in transit | Transaction log only |
| PIN Block | SAD | Terminal → Network | (Online PIN only) | TLS 1.3 in transit | ❌ Never stored (ephemeral) |
Critical PCI DSS notes:
- ARQC (SAD): Validated during transaction, never persisted in database
- Transport PIN Block (SAD): For online PIN verification (ATMs and magstripe), never stored as a separate transport artifact
- Offline PIN: Verified by chip only (EMV), Green-Got never receives PIN or PIN block
- Track Data: Magstripe supported (e.g., US gas stations) but track data never stored - ephemeral during transaction only
- Magstripe CVM: Signature or Online PIN supported
8.2 Data Storage (Green-Got Database)
What Green-Got stores (encrypted at rest):
| Data Element | Classification | Storage Location | Encryption Method | Retention Policy | Justification |
|---|---|---|---|---|---|
| PAN | CHD | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime + 7 years | Required for all transactions |
| Cardholder Name | CHD | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime + 7 years | Required for transactions |
| Expiry Date | CHD | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime + 7 years | Required for transactions |
| PVV | SAD | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime | Online PIN verification |
| Issuer-side encrypted PIN block | SAD | Database (encrypted) | Inner PIN block retained under PEK_MC_AUTH, then encrypted at rest with ChaCha20-Poly1305 (KMS KEK) | Card lifetime | PIN reveal in dedicated iframe, Exceet transmission, EMV PIN change preparation |
| Green-Got online PIN counter | Security state | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime | Online PIN blocking independent from chip PTC |
| Mirrored chip PTC | Security state | Database (encrypted) | ChaCha20-Poly1305 (KMS KEK) | Card lifetime | Offline PIN block status based on EMV chip data when available |
| Last ATC | Non-sensitive | Database (plaintext) | Not encrypted | Card lifetime | Anti-replay protection |
What Green-Got NEVER stores:
| Data Element | Classification | Policy | Reason |
|---|---|---|---|
| Track Data | SAD | ❌ Never stored | Magstripe supported but track data ephemeral only |
| ARQC | SAD | ❌ Never stored | Ephemeral, validated during transaction only |
| ARPC | SAD | ❌ Never stored | Generated per transaction, not persisted |
| Transport PIN Block | SAD | ❌ Never stored as a separate transport artifact | Generated on-demand for network transport or manufacturer communication |
Issuer exception (PCI DSS):
- As card issuer, Green-Got is permitted to store issuer-side PIN reference data and the issuer-side encrypted PIN block when justified for issuing operations
- Merchants and service providers are prohibited from storing these
- Green-Got is the authoritative source for card credentials
8.3 Data Flow Entry and Exit Points
CHD Ingress (Entry to Green-Got):
- Card chip → Terminal → Mastercard → Green-Got
- PAN, Cardholder Name, Expiry (every transaction)
- Encrypted TLS 1.3 in transit
CHD Egress (Exit from Green-Got):
- Green-Got → Mastercard (authorization response)
- No CHD in response (approval code only)
- Green-Got → Arkéa (clearing files)
- PAN, transaction amount (encrypted files, 90-day retention)
SAD Flow:
- ARQC ingress: Card → Terminal → Mastercard → Green-Got (validated, discarded)
- ARPC egress: Green-Got → Mastercard → Terminal → Card (generated, sent, discarded)
- Transport PIN Block: Terminal → Green-Got (online PIN only, validated, discarded)
- Issuer-side encrypted PIN block: Stored by Green-Got under the issuer model described in 3_pin.md; translated on demand for reveal PIN, Exceet transmission, or chip PIN change preparation
8.4 Encryption Architecture (Reference)
For detailed encryption architecture, see Cryptography & Key Management.
Summary:
- Storage encryption: AWS KMS (ChaCha20-Poly1305) - KEK + data keys pattern
- ARQC validation: AWS Payment Cryptography (IMKac key in HSM)
- Session key derivation:
Session Key = DeriveKey(IMKac, PAN, ATC) - Clear CHD/SAD: Only in CBS memory during validation, immediately zeroized
9. Used Components
9.1 Cardholder Data
-
PAN → 4_pan.md
- Transmitted in authorization request
- Used for ARQC session key derivation
- Retrieved from card record for validation
-
Card expiry date → 13_card_issuance.md
- Validated during authorization
- Included in EMV data
9.2 Cryptographic Components
-
ARQC/ARPC → Core EMV cryptography
- Generated/validated using IMKac
- Proves card authenticity and transaction integrity
-
IMKac → 2_cryptography_key_management.md
- Master key for ARQC generation/verification
- Stored in AWS Payment Cryptography HSM
- Shared Green-Got ↔ Chip (via Exceet during personalization)
-
Session Key Derivation
- Derived from IMKac + PAN + ATC
- Unique per transaction (due to incrementing ATC)
- Handled by AWS Payment Cryptography
9.3 PIN Components
-
PIN → 3_pin.md
- Offline PIN: Verified by chip (chip contact transactions)
- Online PIN: Verified by Green-Got (mainly ATMs)
- Not required for contactless < €50
-
PVV (PIN Verification Value) → 3_pin.md
- Used for online PIN verification only
- Not used in standard EMV chip contact (offline PIN)
-
Issuer-side encrypted PIN block → 3_pin.md
- Retained under
PEK_MC_AUTH - Used for PIN reveal, Exceet translation, and EMV PIN change preparation
- Retained under
9.4 AWS Payment Cryptography
Green-Got uses AWS Payment Cryptography for all EMV cryptographic operations:
VerifyAuthRequestCryptogram- Validate ARQCGenerateMac- Generate ARPC- HSM-backed key storage (IMKac never leaves HSM)
- PCI DSS compliant
10. Related Documentation
10.1 Internal Documentation
- 3_pin.md - PIN verification, offline/online PIN flows
- 2_cryptography_key_management.md - Key management, IMKac lifecycle
- 4_pan.md - PAN lifecycle and management
- 11_mdes_transaction.md - Mobile wallet transactions
- 12_atm.md - ATM withdrawal flows
- 13_card_issuance.md - Card personalization and issuance
10.2 External Standards
- Mastercard DMAS
- PCI DSS v4.0: Payment Card Industry Data Security Standard
- AWS Payment Cryptography API: VerifyAuthRequestCryptogram documentation
999. Uncertainties
This document reflects all the questions that still need to be solved and for which we wrote some documentation but without certainty.
- Pin blocked: Can we change the behavior in a way that it’s not swallowed by the ATM? TBD.
- Contactless + PIN: we considered it’s possible to have a POS asking for PIN if the amount during the contactless operation is more than 50€. Apparently, some modern terminals support contactless PIN entry (card can be removed after tap, PIN entered on terminal keypad using card data already in terminal memory). However, standard European behavior is to prompt “Insert card” for amounts ≥ 50€, converting to chip contact transaction.
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.
- A personal account is held by a natural person (
physical_person). - A business account is held by a legal entity (
legal_entity) — the company. The company owns the account and the funds; the humans involved are participants, not owners.
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:
- Shared personal account: the owner (holder, and also a participant with the Owner role) shares access with a spouse (Member) and an external accountant (Accountant).
- Business account: the company is the holder; the people are participants — a CEO (Owner), a manager who can see and validate their team’s spending, employees who manage only their own card, an accountant who can only export.
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.
| Role | What it can do (summary) |
|---|---|
| Owner | Full control: manage the account, transfers, cards and participants; export. |
| Admin | Day-to-day management of transfers and cards; read/write/export; cannot transfer ownership. |
| Manager | A Member who can also see and validate the spending of the people who report to them. |
| Member | Read the account, manage their own card, initiate transfers (subject to approval). |
| Accountant | Read and export transactions and accounting data only — no card, no transfer. Designed for an external accounting firm. |
| Viewer | Read 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:
- the legal holder — always (the person, or the company for a business account); plus
- every participant flagged
appears_for_payee_verification.
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:
- Shared personal account: the holder + any flagged co-titulaire (e.g. a spouse). A payment addressed to the spouse at the shared IBAN matches; one addressed to the household’s accountant does not.
- Business account: normally just the company. Employees — even directors — are participants, not payees, so the flag stays off and VOP returns only the company name.
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
Legal persons (businesses)
KYB
Initial
Updates
Risk Assessment
Initial
Updates
Screening
Initial
Updates
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:
- The marker must be on its own line.
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
downmigration correctly reverts anupmigration
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:
- Read Queries: These queries are used to fetch data from the database without modifying it. They can be executed concurrently without any locking issues.
- Write Queries: These queries modify data in the database. They may require locking mechanisms to ensure data integrity during concurrent operations.
- Transactional Queries: These queries are executed within a transaction block, ensuring that a series of operations are completed successfully before committing the changes to the database. Transactions help maintain consistency and can involve locking behavior depending on the isolation level.
Executor type
Depending on the type of query being executed, we use different executor types:
- PgExecutor: This is used when a query can either be a single query or part of a transaction. It provides flexibility in executing both read and write queries.
- PgTransaction: This is specifically used for executing queries within a transaction. It ensures that all operations within the transaction are treated as a single unit of work, providing atomicity and consistency.
- PgPool: This is used for executing single, standalone queries that should
not require transaction support. It is typically used for read queries. This
executor type is used in very few places in the codebase, as we prefer using
PgExecutorfor its flexibility.
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:
- FOR UPDATE: This lock is used when row keys are selected for update. It prevents other transactions referencing the same rows, by reference or by key, from acquiring locks that would conflict with the current transaction’s intended updates.
- FOR NO KEY UPDATE: This lock is similar to
FOR UPDATEbut allows other transactions to acquire locks that do not conflict with the current transaction’s intended updates. It is used when the rows are selected for update but not for key updates.
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.
-
We prob want our own wrapper and
fakewhich enabled theDummymacro andgardewith enabled theValidatemacro so we can define data validation and generation together because very often validation can just be used for generation -
For often used data like
Emailwe can also create a newtype and then the fake data generation behavior is like forStringpredefined. -
One issue with the CLI and seeding is that the CLI is prebuild so either we need to somehow detect if seeds need to be rebuild and trigger a rebuild of the CLI or we always dynamically build and execute seeds. My current take on this would be detect if seeds need to be updated rebuild the CLI and update all machines with the new binary.
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
- Seed multiple retail users with account data.
- Keep writes highly parallel.
- Make every seed step a Postgres batch operation.
- Run all scenario batch operations concurrently.
- Avoid wrapping the full scenario in a transaction.
- Keep seed data idempotent.
- Keep production
list_accountandget_accountread paths unchanged. - Keep default seed data focused on useful happy-path accounts.
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:
- anchor users with 1, 2, and 3 visible accounts,
- users with one, two, and three account rows across the wider data set,
- a negative balance account,
- suspended, blocked, and closed bank account rows,
- an account without an IBAN,
- users with no account.
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:
- Build all scenario graphs in memory.
- Execute one Postgres batch statement per seed step.
- 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:
- all IDs and values are generated before database work starts,
- each logical table write is a single round trip,
- all table-level writes are started together with
tokio::try_join!, - FK checks are disabled by the seeding pool, so insert order does not need to serialize the graph.
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:
- one
app_userper scenario, - one email login identifier per scenario,
- one password credential per scenario.
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:
- accounts belonging to the mock-authenticated
app_user, - linked to a bank account,
- with a primary IBAN,
- with the same full account shape already planned for the frontend.
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
- Keep physical storage table name
retail_account. - Keep public/domain/API name
Account. - Use
iban.primary, notiban.is_primary. - Do not create
retail_account_change_capture. - Do not add a custom
Serializeimplementation to the internalAmounttype. - Use a separate frontend/API amount DTO when exponent needs to be communicated.
- Do not store exponent. Derive it from currency.
- Use explicit
updated_at = NOW()updates in SQL where needed.
Test Plan
Implementation is separate from tests, but the eventual verification should cover:
- Run
cargo check. - Run the local database reset/seed flow.
- Find
retail.account@green-got.com. - Call
list_accountwith that user’s mock-auth context. - Verify exactly the expected account is returned.
- Call
get_accountfor that account ID. - Verify the returned account matches the list result.
- Verify the returned balance shape:
{ "value": 14940, "currency": "EUR", "exponent": 2 }
- Run seeds twice and confirm the result remains idempotent.
Open Implementation Decisions
- Exact module location depends on whether the new
accountmodule already exists at implementation time. - Batch bank account insertion may need a seed-only helper instead of reusing
create_bank_account. - Conflict targets should follow the actual unique indexes after the migration lands.
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:
-
Domain Layer: This is where your business logic lives. You’ll define your entities, value objects, aggregates, and business rules here.
-
Application Layer: This coordinates tasks and orchestrates use cases. It doesn’t contain business logic itself, it just wires up domain objects. When the application layer only contains use cases, you can use a
use_casesfolder directly instead of nesting it underapplication/use_cases. If you later add more application-level concerns (DTOs, error mapping, etc.), you can introduce the parentapplicationfolder. -
Infrastructure Layer: This deals with technical concerns: databases, HTTP clients, file systems, external services, and so on. When you start a new module or crate following this approach you can often start a
storesfolder. Later on, as your needs grow, you might introduce more infrastructure components likeadaptersorports. In which case you can create a parent folderinfrastructure. -
Presentation Layer: This is the entry point of your application, as exposed to the clients. It can be an HTTP API, or a CLI for example.
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:
-
Entity: An object that has an identity and evolves over time (e.g.
User,Invoice). Two users with the same data are still different if they have different IDs. -
Value Object: An object defined by its data, not its identity. It’s immutable and replaceable (e.g.
EmailAddress,Money, orPeriod). Two value objects with the same values are considered equal. -
Aggregate: A cluster of entities and value objects that are treated as a single unit for data changes. One entity is the “aggregate root”, and it’s the only part of the aggregate that other code can reference directly. For example a
UserInvoicesaggregate might contain aUserand a list ofInvoiceentities.
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
docker_composeconfiguration for local setupclickhouselocal setupdocker-compose.ymlto start all our containers
srcactionsutilities for ci, cli and cdcipure rust ci instead of github actionscicdgithub actions self hosted runners to deploy the legacy applications => will be deleted when the legacy is decomissionedcligg cli to start all our processes (tests, deployments…)clientsintegrations of our providers and partnersconfigstore values and constants that are used in the entire stack ; those values could be stored in database, but we store them in memory for faster access and the database is regularly pulled to check if there was updatescontextTODO: decommissionate and remove, each application will have their own contextdata_warehouseclickhouse related content for the data lake (include migrations)dbpostgresql related content (includes migrations)developmentutility cratedocsgenerate the documentation filedocumentationfor chunks of documentation that do not fit next to codedomaincontainer for domains packagesenvenv variables and keyserrorTODO : remove this folderevent_busutility library for our event businfrastructurepulumi code to deploy our infra on AWS (the server + the surroundings)macroswraper for our macrosmodelsshared models as utilitirespdfto manage documentsproc_macrosproc_macros_utilsqueueutility queueing libraryschedulerscheduler utilityscriptsservermain function to start all the applicationsserviceslayer of indirection => TODO : colocate the service layer with the logic code (cicd goes into the ci crate) and delete most of the code here. Keep a service trait and put it into /utilstelemetrytelemetry utilitytemporalTemporal integration crate and shared alpha SDK adaptertestingtesting utilitiesutilsbasic shared utilitieswebto add some ui for internal needs
.tomlfiles for the configuration
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
- Reset the database with:
gg reset
[!CAUTION] This will delete the database if it exists and create it again ; then apply the migrations.
- Run all the migrations with:
gg migrate(aliasgg m)
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:
position: The position of the markdown file in the documentation tree. This is optional and can be used to arrange the order of your elements in the end document.path: The path of the markdown file in the documentation tree. It’s used to build the table of content and the titles of the sections.vanta: Optional Vanta metadata. Usevanta.urlto bind the page to a Vanta resource.vanta.status: Optional enum. Supported values areMissing,Deactivated, andSubmitted.vanta.status: Deactivated: Used for Vanta test URLs. The markdown body is used as the deactivation reason.vanta.status: Submitted: Used for Vanta document URLs. The linked Vanta document is submitted during docs sync. When a requirement is not applicable, keep the document submitted and explain the rationale in the markdown body.vanta.status: Missing: The default when omitted.
Notes:
- You should not go beyond a level 6 depths
- The path should be unique
- You don’t need a file for each level of the path. You can have a file with a path of
['Usage', 'CLI']and no file with a path of['Usage']. The cli will add the missing levels in the table of content and in the body of the page. - This system is used to regroup documentation elements that belongs together logically but that are placed in different sections of your codebase.
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.
- PostgreSQL is running on port
5432 - ClickHouse is running on ports
8123(HTTP) and9000(native) - Temporal is running on port
7233. You can access the Web UI at http://localhost:8233/namespaces/default/workflows - Mongo is running on port
27017
Staging
The staging environment is the one used for testing. You can access the logs in Grafana.
Get your rust environment ready
-
Install Tailscale
-
Install Docker (Docker Desktop or OrbStack for example)
-
Install Rust:
curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly -y
- 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
-
Run
code /Library/LaunchDaemons/limit.maxfiles.plist -
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>
-
Save as sudo
-
Reboot
-
Run
launchctl limit maxfilesto 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
- The client sends a request to the retail API from his mobile device.
- 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) - logging module – From now, the logging module will log everything that happens to the request.
- context module – The context middleware of the retail API adds a context to the request (request_id, user_id, etc.).
- 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) - retail API – The retail API receives the request and routes it to the
transaction details endpoint.
can throw an error (404) - retail API – The transaction detail endpoint calls the useCase method in the retail API (in application) module.
- 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.
- domain – The rules are checked to allow the transaction details to be seen
by the user.
can throw an error (401, 403, 500) - 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.
- store – The transaction store calls the getTransactionDetails method in the
database (in database) module.
can throw an error (404) - Result is returned to the end client.
MasterCard wants to know if we authorize a transaction
- MasterCard sends a request to the MasterCard WebHook from their servers.
- 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) - logging module – From now, the logging module will log everything that happens to the request.
- context module – The context middleware adds a context to the request (request_id, user_id, etc.).
- 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) - mastercard webhook – The MasterCard WebHook receives the request and routes
it to the transaction authorization endpoint.
can throw an error (404) - 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.
- integration – The 8583 module translates the message into an object corresponding to our internal model and sends back to the useCase method.
- 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.
- domain – The rules are checked to authorize or decline the transaction.
- store – The required stores are called to gather the necessary information to authorize or decline the transaction.
- 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) - store – The balances and last transaction request date are updated in the account store.
- mastercard webhook – The usecase calls the integration to translate the response into an ISO 8583 message.
- integration – The 8583 module translates the message into an ISO 8583 message.
- mastercard webhook – The response is sent back to MasterCard.
Asynchronous from 12.
- domain – The domain starts a workflow in temporal to create the transaction.
- store – The ledger is updated with the new transaction.
- integration – The transaction is forwarded to HawkAi
- domain – The transaction enrichment module is called to enrich the transaction.
- store – The transaction is updated with the new information.
An employee validates a new client account creation
- The employee clicks the button to validate the account creation and the back office send a request to the private API.
- 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) - logging module – From now, the logging module will log everything that happens to the request.
- context module – The context middleware adds a context to the request (request_id, user_id, etc.).
- 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) - private API – The private API receives the request and routes it to the
validate account creation endpoint.
can throw an error (404) - private API – The validate account creation endpoint calls the useCase method in the private API (in application) module.
- 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.
- domain – The rules are checked to allow the account creation to be
validated.
can throw an error (401, 403, 500) - domain – The store are called for synchronous updates ; then the result is sent to the useCase method.
- private API – The useCase method returns the result to the end client.
Asynchronous from 5.
- private API – The api calls the domain to feed the audit trail.
- domain – The domain starts a workflow to feed the audit trail.
- store – The audit trail is updated with the new information.
Asynchronous from 10.
- domain – The domain starts a workflow in temporal to create the account and to start the integrations.
- store – The account is created in the database.
- 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
cargo tcargo testscargo test --workspace
(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.tomlfile.
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:
- Eliminate ambiguity in terminology across different contexts.
- Ensure clear communication between technical and non-technical team members.
- Standardize language used in documentation, code, and customer-facing materials.
- Help our people to get to know the lingo and the acronyms.
This glossary contains:
- Preferred terms and their definitions
- Forbidden terms and their recommended replacements
- Context-specific usage notes
- Cross-references between related terms
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:
- See card details and IBAN;
- Add or edit a beneficiary;
- Accept or revoke a mandate;
- Make a wire transfer;
- Accept a 3DS verification;
- Update card status and limits.
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
- Allow incoming HTTP traffic from the load balancer security group to the instance
- Allow outgoing traffic everywhere
Database security Group
- Allow incoming TCP traffic on port 5432 from the instance security group
- Disallow all outgoing traffic
Load balancer security Group
- Allow all incoming HTTPS traffic from AWS Cloudfront
- Allow outgoing http traffic to our instance security group
- In addition to the security group we also ask the load balancer to only forward requests that contain a certain header that we set via Cloudfront
With this setup we delegate most of the DDoS protection up to Layer 6 to AWS Shield
Layer 7 DDoS protection
- Limit the ASN based on the
CloudFront-Viewer-ASNheader - Limit server to server connections to using an IP allowlist
- Block certain geolocations
- Block “bad ips”
- IP based rate limit
- User based rate limit
- 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 keys are available for business and for invidual banking offers
- We have a user table and a organisation table. For individual banking clients they simply will have the field organization_id set to
None.
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 :
- 100% isolated ; calls to providers are mocked. The monitoring is local when enabled.
- Relaxed security controls. The developper has access to all the servers and the data.
- They use mocked data OR they can pull anonymised data from the production database to debug real wrold cases. No real sensitive and personal data can arrive on this enviroment.
- Unstable and not fit for production.
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 :
- Plugged to mocked providers
- Has relaxed security rules so developpers can SSH into the servers and check the database for debugging purposes
- Is continuously fed with anonymised data.
- Short lived.
- Low attention to the performances and scalabilty.
- No alerting for incidents except for testing purposes.
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 :
- Plugged to mocked providers
- Has relaxed security rules so developpers can SSH into the servers and check the database for debugging purposes
- Is continuously fed with anonymised data.
- Long lived ; always on an available.
- Low attention to the performances and scalabilty.
- No alerting for incidents except for testing purposes.
Production enviroment
Hosts the live application. Processes actual perosnal data and payment data includign card data.
Characteristics :
- Strict controll policy for accesses
- Plugged to actual providers
- Fully optimized for performance and secutiry
- Heavily monitored and logged.
- Alerting are in place.
- Implement :
- Network segmentation : this is managed at the host level. In AWS we set security groups that restrain inbound and outbound traffic for each part of the network.
- Encryption at rest is setup in our AWS database (Aurora)
- Strong ACLs
- Key rotation with AWS KMS
- Backup system and disaster recovery processes
General considerations :
- We use feature flags and partial roll outs to avoid complete failures.
- The production environment is spread accross multiple instances of the service to avoid a single point of failure.
- This environement is subject to regular vulnerability scans and penetation testing.
Keys
We implement a robust key management system to ensure compliance with security standards.
Master Key
- The master key is used to encrypt / decrypt all the other secrets and key.
- The master key is stored in AWS Parameter Store.
- It is encrypted at rest and only accessible by the server during deployment.
- There is one master key per environment
Key Generation and Distribution
- It starts with a member of the team that has a secret or a key that he wants to add or to rotate
- 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.
- The service returns an encrypted version of the key.
- 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:
- Only encrypted versions of the keys are stored, not plaintext.
- Access to the repository is restricted and audited.
- Even if the repository is compromised, the attacker cannot use the encrypted keys without the master key.
Key Deployment and Usage
- During server deployment, the master key is retrieved from AWS KMS.
- All secrets (encrypted keys) are decrypted using the master key.
- Decrypted keys are stored in server memory for use.
- Master key is discarded
Encryption Algorithm
We use ChaCha20-Poly1305 for encryption, which is:
- A modern, high-speed symmetric encryption algorithm.
- Designed to be more resistant to timing attacks compared to AES.
- Provides both confidentiality and authenticity (encryption and MAC) in a single pass.
- Widely regarded as secure and recommended by cryptography experts.
Crypto Period and Master Key Rotation
- The master key doesn’t need frequent rotation because:
- It’s used only to encrypt/decrypt other keys, not data directly.
- It’s stored securely in AWS KMS with limited access.
- The risk of exposure is minimal due to its limited use.
- However, an annual rotation schedule is implemented.
Third party and internal secrets rotation
- We follow the rotation life cycle of our providers. When they give us a new version of the key, we use it.
- If we feel that the key has been use over its crypto period, we require a new key.
Benefits
- Improved developer experience: Easy addition of new secrets.
- Enhanced deployment stability: Keys are version-controlled with the code.
- Increased security: Clear-text keys are never stored in the repository.
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 :
- Server hosting : AWS
- Database hosting : AWS (Aurora)
- Monitoring : Graphana cloud
- Guaranteed execution engine : Temporal cloud
- Database analytics : clickhouse cloud
- Code repository : Github
- VPN : tailscale
- CI hosting : Hetzner
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)
- Overall accountability
- Approving accesses to anything
- Approving third party providers and budgets
- Approving policies
Access
- Access management and rights
IT Security team
- Proposing, developing and maintaining security policies and procedures
- Implementing and managing security controls
- Conducting regular security assessments and penetration testing
- Monitoring security events and responding to incidents
- Providing security guidance to other teams
- Managing access control systems
- Overseeing encryption standards and implementation
- Liaising with external auditors and QSAs (Qualified Security Assessors)
- Conducting internal technical audits and assessments
Access
- Main AWS account and keys
Development team
- Writing code
- Implementing security features in applications
- Participating in code reviews with a focus on security
- Addressing security vulnerabilities identified in the code
- Collaborating with the security team to implement security controls
Access
- Main repository to push / pull
Operations team
- Managing and maintaining the production environment
- Implementing and maintaining system hardening standards
- Managing patching and updates for systems and applications
- Configuring and maintaining firewalls and other network security devices
- Monitoring system performance and availability
- Implementing and managing backup and recovery processes
- Collaborating with the security team on incident response
Access
- Production database
- Production servers via SSH
- CI / CD : manage deployments and push code to production
Compliance Officer
- Overseeing the organization’s PCI DSS compliance program
- Conducting internal audits and assessments
- Tracking and reporting on compliance status
- Advising other teams on compliance requirements
- Maintaining documentation of their processes
- Coordinating compliance training programs
Access
- No technical access
Customer Care Team
- Providing front-line support to customers
- Accessing and managing customer data within approved systems
- Adhering to data protection policies and procedures
- Identifying and escalating potential security or compliance issues
- Participating in regular security awareness training
- Maintaining confidentiality of customer information
- Following protocols for handling customer inquiries about data security
Access
- No technical access
Security
Technology stack
Incoming request from clients and providers goes through:
- AWS Cloudfront
- AWS Shield (DDoS protection)
- AWS WAF
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
- A person can have only one insurance per account
- A person can have multiple insurance if they have multiple accounts.
- The insurance policy id is unique.
Insurance ID format
- firstName_lastName_YYYY_MM_DD_accountNumber_insuranceProductId
YYYY_MM_DD: the date of the subscriptionaccount_number: last 11 numbers of the IBANinsuranceProductId: from Owen
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:
- Customer may choose not to take the card.
- If customer chooses the card, insurance is automatically included.
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
- Describe the way we store this Green-Got side
- reason
- id
- date
- …
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:
contractType: in the get product (or cached, TBD)channel: in/owen-products/contract_type/{{contract_id}}> channels > [0]contractPeriod: in/owen-products/contract_type/{{contract_id}}> contractPeriodType > contractPeriod [0]contractPeriodType: in/owen-products/contract_type/{{contract_id}}> contractPeriodType [0]priceType: in/owen-products/contract_type/{{contract_id}}> priceTyperateType: in/owen-products/contract_type/{{contract_id}}> priceType > rateType (choisir)paymentPlan: in/owen-products/contract_type/{{contract_id}}> paymentPlan [0]paymentType: in/owen-products/contract_type/{{contract_id}}> paymentType [0]renewableContract: in/owen-products/contract_type/{{contract_id}}> renewableContract [0]transactionHorodate: DateTime ISO optionallastname: from our databasefirstname: from our databasebirthdate: from our databaseplaceOfBirth: from our databaseaddress: from our databasezipcode: from our databasecity: from our databasecountry: from our databasephone: from our databaseemail: from our databasecountryOfBirth: from our databaseidTransaction: unique ID that will be use by Green-Got and owen to communicate about the contract.
Once this is done, the customer gets an email from Owen to activate their account.
idTransaction format: IBANXXXXX-ddmmyy-firstName-lastName
TODO
- Decide how to format the
idTransaction. Note : the customer will see this id in clear in their owen space. - Describe the way we store this Green-Got side
- reason
- id
- date
- …
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
- Since the data from these requests are not supposed to change (or we would be informed way ahead if it were the case), what should we do with them ? Calling Owen for each adhesion is possible, but a bit slow. Caching is faster, but it means we have something to do.
Accounts & cards
Account products
Overview:
- Products are hardcoded in the codebase.
- All products are visible in the backoffice for consultation.
- Products are selectively exposed to end customers based on availability.
The periods for billing are not the first/end of the month. You can start a period on any day.
Product Data Structure:
allowed_fiscal_residencies: List of allowed fiscal residencies (3-letter codes)allowed_delivery_address: List of allowed delivery address (3-letter codes)allowed_nationalities: List of allowed nationalities (3-letter codes)billing_period: Enum – values:YEARLY,MONTHLYbilling_period_sibling: string : the id of the sibling productactivation_date: activation date (ISO 8601 format), possibly in the futuredebit_time: Enum – values:end_of_period,begining_of_perioddefault_locale: Default language code ex:enexpiration_date: Date when product becomes unavailablefeature_flag: if the product is only available for customers with a feature flag activatedid: Technical identifierinsurance_id: optional, the id of the related insurance productname: Translation object ex:{fr: "essentiel", en: "essentials"}onboarding_topup: Amount and currency ex:{value: 6.90, currency: "EUR"}this is used if the customer does not have an account with enough money to pay the first 3 monthsperks: List of added perks ex:{fr: "- a\n- b", en: "- a\n- b"}price: Amount and currency ex:{value: 6.90, currency: "EUR"}promote_before_activated: boolean ; before a product is activated, we can show it for the end customer in the applicationslug_name: String, slugyfied version of the default language nametype: Enum – values:BUSINESS,PERSONAL
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
- Backoffice: All products visible
- End customer: Selective visibility based on availability
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.
Products List
Individual current account legacy
allowed_fiscal_residencies:allowed_nationalities:billing_period:MONTHLYdebit_time:end_of_perioddefault_locale:enexpiration_date: 2025/01/01name:{fr: "compte courant", en: "current account"}onboarding_topup:{value: 30.00, currency: "EUR"}price:{value: 6.00, currency: "EUR"}slug_name:current_legacytype:PERSONAL
Sole trader account legacy
allowed_fiscal_residencies:allowed_nationalities:billing_period:MONTHLYdebit_time:end_of_perioddefault_locale:enexpiration_date: 2025/01/01name:{fr: "compte micro entreprise", en: "sole trader account"}onboarding_topup:{value: 30.00, currency: "EUR"}price:{value: 6.00, currency: "EUR"}slug_name:sole_trader_legacytype:BUSINESS
Shared Account legacy
allowed_fiscal_residencies:allowed_nationalities:billing_period:MONTHLYdebit_time:end_of_perioddefault_locale:enexpiration_date: 2025/01/01name:{fr: "compte partagé", en: "shared account"}onboarding_topup:{value: 30.00, currency: "EUR"}price:{value: 8.00, currency: "EUR"}slug_name:shared_legacytype:PERSONAL
Shared Account additional card holder legacy
allowed_fiscal_residencies:allowed_nationalities:billing_period:MONTHLYdebit_time:end_of_perioddefault_locale:enexpiration_date: 2025/01/01name:{fr: "compte partagé, carte additionnelle", en: "shared account additionnal card"}price:{value: 4.00, currency: "EUR"}slug_name:shared_legacy_additionnal_cardholdertype:PERSONAL
Individual current account essential monthly version 2024/12/01
allowed_fiscal_residencies:allowed_nationalities:billing_period:MONTHLYbilling_period_sibling: see other idactivation_date: 2025/01/01debit_time:begining_of_perioddefault_locale:enid: idinsurance_id: idname:{fr: "essentiel", en: "essential"}onboarding_topup:{value: 30.00, currency: "EUR"}perks:{fr: "- a\n- b", en: "- a\n- b"}price:{value: 7.90, currency: "EUR"}slug_name:current_essential_monthly_2025_01_01type:PERSONAL
Individual current account essential yearly version 2024/12/01
allowed_fiscal_residencies:allowed_nationalities:billing_period:YEARLYbilling_period_sibling: see other idactivation_date: 2025/01/01debit_time:begining_of_perioddefault_locale:enid: idinsurance_id: idname:{fr: "essentiel", en: "essential"}onboarding_topup:{value: 90.00, currency: "EUR"}perks:{fr: "- a\n- b", en: "- a\n- b"}price:{value: 80.00, currency: "EUR"}slug_name:current_essential_annual_2025_01_01type:PERSONAL
Individual current account premium monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:individual_current_account_premium monthly version 2024/12/01type:PERSONAL
Individual current account premium yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:individual_current_account_premium yearly version 2024/12/01type:PERSONAL
Individual current account ultra monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:individual_current_account_ultra monthly version 2024/12/01type:PERSONAL
Individual current account ultra yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:individual_current_account_ultra yearly version 2024/12/01type:PERSONAL
Shared current account essential monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_essential monthly version 2024/12/01type,PERSONAL`
Shared current account essential yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_essential yearly version 2024/12/01type,PERSONAL`
Shared current account premium monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_premium monthly version 2024/12/01type,PERSONAL`
Shared current account premium yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_premium yearly version 2024/12/01type,PERSONAL`
Shared current account ultra monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_ultra monthly version 2024/12/01type,PERSONAL`
Shared current account ultra yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_ultra yearly version 2024/12/01type,PERSONAL`
Shared current account essential monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:shared_current_account_essential monthly version 2024/12/01type,PERSONAL`
Shared current account additionnal card holder essential monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_essential monthly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder essential yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_essential yearly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder premium monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_premium monthly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder premium yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_premium yearly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder ultra monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_ultra monthly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder ultra yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_ultra yearly version 2024/12/01type:PERSONAL
Shared current account additionnal card holder essential monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Shared current account additionnal_card_holder_essential monthly version 2024/12/01type:PERSONAL
Sole trader current account essential yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Sole trader_current_account_essential yearly version 2024/12/01type:BUSINESS
Sole trader current account premium monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Sole trader_current_account_premium monthly version 2024/12/01type:BUSINESS
Sole trader current account premium yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Sole trader_current_account_premium yearly version 2024/12/01type:BUSINESS
Sole trader current account ultra monthly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:MONTHLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Sole trader_current_account_ultra monthly version 2024/12/01type:BUSINESS
Sole trader current account ultra yearly version 2024/12/01
allowed_fiscal_residencies: []allowed_nationalities: []billing_period:YEARLYbilling_period_sibling: see idactivation_date: 2025/01/01debit_time:end_of_perioddefault_locale:enexpiration_date: nullid: idinsurance_id: idname:{fr: "???", en: "???"}onboarding_topup:{value: ???, currency: "EUR"}perks:{fr: "???", en: "???"}price:{value: ???, currency: "EUR"}promote_before_activated: falseslug_name:Sole trader_current_account_ultra yearly version 2024/12/01type:BUSINESS
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 :
- Check the
expiration_date. The product must not be expired. - Customer nationnality must be in
allowed_nationalities - Customer fiscal residency must be in
allowed_fiscal_residencies - Customer type must correspond
BUSINESSorPERSONALdepending on the type of account.
Account cancellations
Cancellations originate from the end customer.
TODO
- write the process
- see the closing process
Account closing
Closings originate from green-got. Reasons for closing an account include compliance issues and overdraft situations.
Cancellation data structure:
id: Unique identifier for the closingaccount_id: ID of the account being closedreason: Enum – values:COMPLIANCE,OVERDRAFTdetails: Additional information about the closing reason manual messageinitiated_by: ID of the customer or system initiating the closinginitiated_at: Timestamp of when the closing was initiatedclosed_at: Timestamp of when the account was fully closedstatus: Enum – valuesPENDING,COMPLETED,CANCELLEDcompliance_case_id: Reference to compliance case (if applicable)overdraft_amount: Amount of overdraft at closing (if applicable)
Proposed workflow:
- Initiation: in the back office, the officer creates the request with initial data
- Review:
- For compliance closings: compliance team reviews the case (how much verification is needed)
- For overdraft closings: who verifies the status ?
- Notification: account holder is notified of account closure
- Account Freezing: restrict further transactions on the account
- Balance Settlement:
- For overdraft: attempt to recover owed amount
- For compliance: handle remaining balance as per regulations
- Update integrations
- customerio
- intercom
- owen for the insurances
- Mastercard ?
- Archive the customer case
TODO
- list all the closing reasons
- Refine the workflow
- What audit trail do we leave ?
- Define data retention conditions
- The “Update integrations” needs to be refined ; what to do for each point ? Probably needs a full documentation for each point.
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
- write examples
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:
- Price calculation: Final Price = Base Price - Discounts - Credit Notes
- Start with the base price of the subscription
- Apply discounts.
- Further reduce the price by subtracting any credit notes ; see
Credit notesfor more details.
- Internal Transfer: If a debit is needed, an internal transfer is made to Green-Got.
- If the customer does not have enough on his account, we try to take it from another current account or wallet
TODO
- Describe the Cron: Provide a detailed explanation of how the cron job operates and its schedule.
- Describe the Model: Outline the data model used for handling transactions and invoices.
- Write the Email: Draft the email template used for sending invoices to customers.
- Write the rules to apply the discounts.
Examples
Discounts
Overview:
- The marketing team has the authority to create discounts.
- Discounts are stored in a database for flexibility and management.
- They should be immutable (to discuss)
Data structure for discounts:
id: Unique identifiername: Translation object ex:{fr: "essentiel", en: "essentials"}description: Translation object ex:{fr: "essentiel", en: "essentials"}type: Enum – values:PERCENTAGE,FIXED_AMOUNTvalue: Amount and currency ex:{value: 6.90, currency: "EUR"}start_date: Date when the discount becomes active (ISO 8601 format)end_date: Expiration date of the discount (ISO 8601 format)applicable_products: List of product IDs this discount applies tousage_limit: Maximum number of times this discount can be used (optional)creator_id: ID of the customer who created the discountcreated_at: Timestamp of discount creation
Creation workflow in the back office
- Marketing team member logs into the discount management system
- Selects “Create New Discount” option
- Fills in the required fields from the data structure
- Submits the discount for creation
Technical Workflow:
- System receives discount creation request
- Validates all input fields:
- Ensures required fields are filled
- Checks for valid date ranges
- Verifies product IDs exist
- If validation fails, returns error messages to the customer
- If validation passes, creates new entry in the discount database
- Generates a unique ID for the discount
- Returns success message and discount details to the customer
TODO
- Refine the data structure
- Refine the workflow to create discounts
- Can we have multiple discounts on the same account ? (I’d say no)
Credit notes
Credit notes can be issued for various reasons, including:
- Overpayment
- Custom commercial gestures
Credit Note Lifecycle:
- If a credit note is partially used, it’s invalidated and a new one is created with the remaining amount.
- Credit notes are stored in a dedicated table.
- They can be applied manually or automatically.
- Automatic application occurs when a customer account is debited for subscriptions or fees.
Credit Note Structure:
id: Technical identifieraccount_id: Associated account (credit note is exclusive to this account)created_at: Creation date (ISO 8601 format)amount: Value and currency (e.g., {value: 1.60, currency: “EUR”})reason: Justification for creationcreator: Optional field for manually created notesparent_id: Optional, references the previous incomplete credit note if applicable
TODO
- Define the detailed workflow for:
- Credit note creation
- Application (manual and automatic)
- Partial usage and recreation
- Expiration handling (if applicable)
- 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)
- 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:
- Customer Selection: Operator selects the specific customer account.
- Action Initiation: Operator chooses the “Add a Credit Note” action.
- Reason Selection: Operator selects the appropriate reason for issuing the credit note.
- Message Addition (Optional): Operator may include an explanatory message if needed.
TODO
- Define and list all possible reasons for issuing a credit note.
- Develop a detailed description of the entire workflow, including:
- Customer interface interactions
- Data validation steps
- Approval process (if any)
- System updates following credit note creation
- Notification mechanisms (to customer and/or internal teams)
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.
- Invoice Creation: An invoice is inserted in the database for the transaction ; but the pdf is not generated.
- 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.
- 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:
- Daily Check: The cron job runs every day to determine if a debit is coming in the next 7 days.
- Funds Verification:
- If a debit is incoming, the system verifies that the customer has sufficient funds in their account.
- If the customer does not have the required funds, we send a notification.
- Those notifications are sent every day until the funds have been added to the account.
- 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
- Workflow: Outline the step-by-step process of the cron job and notifications.
- Model: Describe the data model used for tracking customer accounts and commitments.
- Services: Detail the services involved in the transaction and notification process.
- Write the “unsufficient funds” notification for the customer.
- Write the yearly reminder email.
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
- Check for an existing
downgrade_pendingstatus. If one exists, stop the process here. - Change the status of the current subscription to
downgrade_pending. - 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
- Default customer exists
- Default customer has started a subscription for our most expensive product that is charge at the end of the period on the 10/11/2024
- We are the 02/02/2025
- There is no overdraft
- The customer has 10000 euros on his bank account
Act
- The customer initialize the downgrade to our cheapest product that is charged at the begining of the period
Assert
- The current subscription has a
downgrade_pendingstatus - A workflow has been schedulded to run at the end of the subscription period
Arrange
- We are at the end of the subscription period (10/02/2025)
Assert
- The new subscription is created with a predecessor id of the old subscription
- The initial subscription is now closed with the right status and has a successor id from the newly created subscription
- If the new subscription is a begining of the month subscription, the client has been charged and has now 10000 minus the amount of the subscription on their account
- The mail catcher has an email for the customer
Example: Currently downgrading
Arrange
- Default customer exists
- Default customer has started a subscription for our most expensive product that has a on the 10/11/2024
- We are the 02/02/2025
- There is no overdraft
- The customer has 10000 euros on his bank account
- The customer has engaged a downgrade process for our cheapest product. The downgrade is scheduled to happen on the 10/11/2025
Act
- The customer initialize the downgrade to our cheapest product
Assert
- The old subscription is still running
- No new subscription have been created
- The customer has received an error
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
- 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.
- 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.
- Charge for Elapsed Time: Process the charge for the time the customer has already used.
- Close Current Subscription: Mark the current subscription as closed and assign it a successor ID.
- Create New Subscription: Set up a new subscription and link it to the previous subscription with a predecessor ID.
- Immediate Charge (if applicable): Charge the customer immediately for the new subscription if it is warranted.
- Send Confirmation Email: Send an email to the customer summarizing the changes made during the downgrade process.
Example: Happy path
Arrange
- Default customer exists
- Default customer has started a subscription for our most expensive product that is charged at the begining of the period on the 10/11/2024
- We are the 02/02/2025
- There is no overdraft
- The customer has 10000 euros on his bank account
Act
- The customer initialize the downgrade to our cheapest product that is charged at the begining of the period
Assert
- The new subscription is created with a predecessor id of the old subscription
- The initial subscription is now closed with the right status and has a successor id from the newly created subscription
- The client has been charged and has now 10000 minus the amount of the new subscription and the prorata for the previous subscription
- The mail catcher has an email for the customer
Example: Happy path alternative
Arrange
- Default customer exists
- Default customer has started a subscription for our most expensive product that is charged at the begining of the period on the 10/11/2024
- We are the 02/02/2025
- There is no overdraft
- The customer has 10000 euros on his bank account
Act
- The customer initialize the downgrade to our cheapest product that is charged at the end of the period
Assert
- The new subscription is created with a predecessor id of the old subscription
- The initial subscription is now closed with the right status and has a successor id from the newly created subscription
- The client has been charged and has now 10000 minus the prorata for the previous subscription
- The mail catcher has an email for the customer
Example: Not enough money
Arrange
- Default customer exists
- Default customer has started a subscription for our most expensive product that has a on the 10/11/2024
- We are the 02/02/2025
- There is no overdraft
- The customer has 10000 euros on his bank account
- The customer has engaged a downgrade process for our cheapest product. The downgrade is scheduled to happen on the 10/11/2025
Act
- The customer initialize the downgrade to our cheapest product
Assert
- The subscription is still active
- No new subcription have been created
- The customer got an error in the application
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
id: technicalcreated_at: Datestatus: EnumSubcriptionStatusproduct_id: the id of the related productcustomer_id: the id of the related customercancel_id: optionnal, the id of the cancel process from the complianceprevious_subscription_id: optionnal, the id of the previous sub if there was a downgrade or an upgradesuccessor_subscription_id: optionnal, the id of the successor sub if there was a downgrade or an upgradedowngrade_target_id: optionnal, if the status isDOWNGRADE_PENDING, we need to the target product for the downgrade
TODO
- Describe the workflow
- Database Design: Decide whether to add a dedicated subscription database or include subscription details within the end customer object.
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 :
RUNNING: Base statusCLOSED: When the customer has closed their subscriptionCANCELED: When the compliance has canceled the subscriptionUPGRADED: When the customer has upgraded for a higher subDOWNGRADED: When the customer has downgradedDOWNGRADE_PENDING: When the customer has downgraded but is still using the current subscription untill it run out of time
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
- If the newly selected subcription has a payment at the begining of the period, we check that the customer has enough money to pay upfront ; otherwise, we send an explicit error and stop here.
- In a transaction
- The initial subscription is now closed with a closing date at today
- A credit note is created pro rata temporis
- The new subscription is created with a predecessor id
- The initial subscription has now a successor id
- If the new subscription is for a product with a begining of period payment, the customer is charged (minus the credit note, but this is detailed in the related workflow)
- The customer gets an email to summary the action
Example: Happy path to a begining of period payment
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product with a begining of the month payment period on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euros on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product with a begining of the month period payment
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- A credit note has been created and is now consumed
- The customer now has 10000 minus the price of the subscription if this is a begining of the month subcription + the amount of the credit note created
- The mail catcher has a summary email for the customer
Example: Happy path to an end of period payment
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product with a begining of the month payment period on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euros on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product with a begining of the month period payment
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- A credit note has been created and is available for the next payments
- The customer now has 10000
- The mail catcher has a summary email for the customer
Example: Out of money
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product on the 10/11/2024
- We are the 02/02/2025
- The customer has 1 euro on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product
Assert
- The old subscription is still running
- No new subscription have been created
- The customer has received an error
Example: Currently downgrading
Arrange
- Default customer exists
- Default customer has started a subscription for our second cheapest product on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euro on their account
- The customer has engaged a downgrade process for our cheapest product. The downgrade is scheduled to happen on the 10/11/2025
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- The customer now has 10000 minus the price of the subscription if this is a begining of the month subcription
- The mail catcher has an email for the customer
- The downgrade workflow scheduled for the 10/11/2025 has been cancelled
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
- We check that the customer has enough money to pay for the previous subscription pro rata temporis. Additionally, if the newly selected subcription has a payment at the begining of the period. We check that the customer has enough money to pay upfront ; otherwise, we send an explicit error and stop here.
- In a transaction
- The charge for the previous subscription is collected pro rata temporis.
- The initial subscription is now closed with a closing date at today
- The new subscription is created with a predecessor id
- The initial subscription has now a successor id
- If the new subscription is for a product with a begining of period payment, the customer is charged.
- The customer gets an email to summary the action.
Example: Happy path to a begining of period payment
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product with a begining of the month payment period on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euros on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product with an end of the month period payment
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- The customer now has 10000 minus the price of the subscription if this is a begining of the month subcription minus the amount of the pro rata temporis for the previous subscription
- The mail catcher has a summary email for the customer
Example: Happy path to an end of period payment
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product with a begining of the month payment period on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euros on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product with a begining of the month period payment
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- The customer now has 10000 minus the amount of the pro rata temporis for the previous subscription
- The mail catcher has a summary email for the customer
Example: Out of money
Arrange
- Default customer exists
- Default customer has started a subscription for our cheapest product on the 10/11/2024
- We are the 02/02/2025
- The customer has 1 euro on their account
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product
Assert
- The old subscription is still running
- No new subscription have been created
- The customer has received an error
Example: Currently downgrading
Arrange
- Default customer exists
- Default customer has started a subscription for our second cheapest product on the 10/11/2024
- We are the 02/02/2025
- The customer has 10000 euro on their account
- The customer has engaged a downgrade process for our cheapest product. The downgrade is scheduled to happen on the 10/11/2025
- There is no overdraft
Act
- The customer asks the upgrade to our most expensive product
Assert
- The old subscription is closed and has a successor id
- The new subscription is created and has a predecessor id
- The customer now has 10000 minus the price of the subscription if this is a begining of the month subcription
- The mail catcher has an email for the customer
- The downgrade workflow scheduled for the 10/11/2025 has been cancelled
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:
- the API calls the domain usecase
get_transaction_detail - the domain calls the store
transaction - the store make a query to the database in SQL
- the response bubles back to the API
If there is an error there we just return an error.
Command example: create an invoice
Flows:
- the API calls the domain usecase
create_invoicewith a payload - the domain starts a transaction
- the store is called for update 1 ; the store calls the db
- the store is called for update 2 ; the store calls the db
- the store is called for update 3 ; the store calls the db
- the transaction is commited
- 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
- the API call the domain usecase
create_recurring_invoicewith a payload - the domain starts a transaction
- insert the recurring invoice to the database
- send the workflow characteristics (arguments and temporality) to postgres for batch treatment
- the response bubbles to the client
- workflows from postgres are batch treated and sent to temporal cloud (arguments are encrypted)
- later on, temporal will trigger the execution of the activity
Cancel example: I want to stop a recurring invoice
- the API calls the domain usecase
cancel_recurring_invoicewith a payload - the domain starts a transaction
- remove the recurring from the database
- send the workflow characteristics (arguments and temporality) to postgres for batch treatment
- the response bubbles to the client
- 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:
- the shared client/runtime adapter
- the encrypted payload codec and data converter
- small repo-owned helpers such as search attribute registration
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
- Raw events: received from an external source like Mastercard, SEPA etc.
- Evented Ledger: the Evented Ledger will store the bare minimum information to keep track of the money movement between accounts.
- Normalized ledger: it will contain normalized transactions, (one for debit, one for credit). It will be optimized for reading, listing transactions per accounts, with all the context needed.
- Balances: in the ledger we will have a table of balances with the account id. Accounts details will be stored out of the ledger. This way if something needs to lock an account somewhere, it doesn’t affect the ledger (we want to work without lock for the ledger)
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
- pending debit
- pending credit
- authorized debit
- authorized credit
- cleared debit
- cleared credit
The pending balances are just used during the time where the authorization checks are performed.
From this we should be able to calculate:
cleared balance=cleared credit-cleared debit. Describes the actual balance of an account at a given timeauthorized balance=cleared credit-cleared debit-authorized debit. Describes the balance available for expenses, the one shown to the user in the current appbalance for limits=cleared credit-cleared debit-authorized debit-pending debit. Describes the balance used for checking limits.
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 id | amount | type | | | Debits cleared | Δ | Debits authorized | Δ | Debits pending | Δ | Credits cleared | Δ | Balance cleared | Balance available | Balance for limits |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| | | 20 | 0 | 0 | 10 | 40 | 20 | 20 | |||||||
| 1 | 10 | RESERVED | | | 20 | 0 | 0 | 0 | 10 | 10 | 40 | 0 | 20 | 20 | 10 |
| 1 | 10 | AUTHORIZED | | | 20 | 0 | 10 | 10 | 0 | -10 | 40 | 0 | 20 | 10 | 10 |
| 1 | 12 | COMPLETED | | | 32 | 12 | 0 | -10 | 0 | 0 | 40 | 0 | 8 | 8 | 8 |
| 2 | 2 | RESERVED | | | 32 | 0 | 0 | 0 | 2 | 2 | 40 | 0 | 8 | 8 | 6 |
| 2 | 2 | REJECTED | | | 32 | 0 | 0 | 0 | 0 | -2 | 40 | 0 | 8 | 8 | 8 |
| 3 | 6 | RESERVED | | | 32 | 0 | 0 | 0 | 6 | 6 | 40 | 0 | 8 | 8 | 2 |
| 3 | 6 | AUTHORIZED | | | 32 | 0 | 6 | 6 | 0 | -6 | 40 | 0 | 8 | 2 | 2 |
| 3 | 6 | CANCELED | | | 32 | 0 | 0 | -6 | 0 | 0 | 40 | 0 | 8 | 8 | 8 |
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:
- Reservation, this event:
- increases the pending debit balance of the source
- increases the pending credit balance of the destination
- a. Authorization, this event:
- decreases the reservation amount from the pending debit balance
- decreases the reservation amount from the pending credit balance
- increases the authorized debit balance of the source
- increases the authorized credit balance of the destination
- b. Rejection, in which case there is no step 3. This event:
- decreases the reservation amount from the pending debit balance
- decreases the reservation amount from the pending credit balance
- a. Completion, this event:
- decreases the reservation amount from the authorized debit balance
- decreases the reservation amount from the authorized credit balance
- increases the cleared debit balance of the source
- increases the cleared credit balance of the destination
- b. Cancellation, this event:
- decreases the reservation amount from the authorized debit balance
- decreases the reservation amount from the authorized credit balance
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:
- entry_id: id
- transaction_id: id
- amount: unsigned
- processed_at: Timestamp
- type: RESERVED | AUTHORIZED | CLEARED | REJECTED | CANCELED
- event_id: id
- step: 1 | 2 | 3 (see Flows section, will just be used in the db for the unique index)
Transaction
A transaction links the ledger entries of one transfer of money. This table will contain the following fields:
- id: id. Our internally defined transaction id.
- ledger_id: id
- external_id (optional): id. This is the id from the transaction given by Mastercard, SEPA, …
- scheme: Mastercard | SEPA | internal… Storing the scheme might enable to avoid an unlikely collision between a SEPA transfer id and Mastercard transfer id (part of the unicity).
- source_account: id.
- destination_account: id.
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:
- the clearing will not find the earlier steps and recreate a reservation and authorization
- the authorized transaction will end up being cleared
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
- https://anvil.works/blog/double-entry-accounting-for-engineers
- https://en.wikipedia.org/wiki/Double-entry_bookkeeping
- https://docs.tigerbeetle.com/concepts/debit-credit/
- https://docs.tigerbeetle.com/coding/financial-accounting/#double-entry-bookkeeping
Separating balances (debit/credit)
- https://docs.tigerbeetle.com/reference/account/
- https://docs.moderntreasury.com/platform/reference/ledger-balances-object
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:
- pacs: Payments Clearing and Settlement messages
- pain: Payments Initiation messages
- camt: Cash Management messages
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:
| Outcome | Meaning |
|---|---|
| MTCH | The name matches the account holder. |
| CMTC | Close match — the holder’s actual name is returned so the customer can confirm. |
| NMTC | No match — the name does not correspond to the account holder. |
| NOAP | Not 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.
- Our responder is a signed inbound webhook (Ed25519 / JWKS). GGBS is the only caller; we verify its signature before answering.
- We answer with the account’s payee identities — the set of names a remitting bank may legitimately pay at that IBAN — as an array of holders.
- GGBS performs the actual name comparison (including the normalisation in VOP – Internal); we only supply the authoritative names. We never see the payer’s submitted name.
What we return as payee identities
For a given IBAN, the payee identities are:
- the account’s legal holder — the natural person (personal account) or the legal entity / company (business account); always returned; and
- 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:
- Personal shared account: the holder plus any co-titulaire flagged as a named party (e.g. a spouse). A payment “to the spouse” at the shared IBAN matches; a payment “to the household’s accountant” does not.
- Business account: only the company. Employees — even directors — are participants, not payees, so a transfer must be addressed to the company name. (A flag can name an individual as a payee party in the rare cases this is legitimate, but it is off by default.)
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:
- the account’s legal holder — a natural person for a personal account, the company for a business account; always exposed; plus
- any participant flagged as a named account party (
appears_for_payee_verification) — a co-titulaire or mandataire. Operational participants (accountant, viewer, employees) are not exposed and never match.
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:
- text is normalized (NFD) => https://unicode.org/reports/tr15/
- everything in transformed to lowercase
- spacing is removed
- non letters characters are removed (like hyphens)
- diacritics and non ascii letters are converted to ascii
CFT
Chronology
How thing happen chronologically speaking
- Throughout the day, transfer files are sent via EBICS to initiate transactions.
- Cut-off for SCT: At the designated cut-off time, funds are moved to the settlement account.
- Cut-off for SDD: Similar processes applied for direct debits.
- Clearing: Funds are cleared through the SEPA network.
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
| Position | Length | Content | Example |
|---|---|---|---|
| 1–2 | 2 chars | Country code (ISO 3166-1) | FR |
| 3–4 | 2 digits | Check digits (MOD-97-10) | 76 |
| 5–end | varies | BBAN (Basic Bank Account Number) | 17928999999904349325888 |
BBAN — France (23 chars)
France’s BBAN follows the RIB format:
| Field | Length | Example |
|---|---|---|
| Bank code | 5 digits | 17928 |
| Branch code | 5 digits | 99999 |
| Account number | 11 alphanumeric | 90434932588 |
| RIB key | 2 digits | 83 |
Branch codes
Our branch codes are published to the “Banque de France”, to be used this way:
| Branch code | Purpose |
|---|---|
| 00000 | Internal (for our tests) |
| 00001 | For GG operational needs: master account, … |
| 00002 | Retail customers |
| 00003 | Business customers |
| 00004 | Institutional partners |
| 99999 | Unit tests (not published) |