mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-15 17:50:22 +00:00
docs: clarify badge lifecycle naming
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
**Status:** implementation-ready
|
||||
**Companion:** [Product and UX plan](2026-07-20-supporter-badges-v2-product.md)
|
||||
|
||||
Payment verification creates a provider-neutral `PaymentCredit`. Badge issuance consumes it. Payment and badge are separate state machines on client and bot.
|
||||
Payment verification creates a provider-neutral `ServiceGrant`. Badge issuance fulfills it. Payment and badge are separate state machines on client and bot.
|
||||
|
||||

|
||||
|
||||
@@ -28,23 +28,16 @@ Payment verification creates a provider-neutral `PaymentCredit`. Badge issuance
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C[Client] -->|one request| R[Service RPC]
|
||||
R --> O[Bot orchestrator]
|
||||
O --> P[Payment service]
|
||||
P --> V[Provider adapter]
|
||||
P --> X[PaymentCredit]
|
||||
O --> B[Badge service]
|
||||
B -->|credential| R
|
||||
R -->|one final response| C
|
||||
C --> K[Core verify and install]
|
||||
C[Client] <-->|Service RPC| B[Badge bot]
|
||||
B <-->|Provider API / webhook| S[Apple / Google / Stripe]
|
||||
```
|
||||
|
||||
| Component | Owns | Must not own |
|
||||
|---|---|---|
|
||||
| Client payment | BBS master key, capability, purchase UI, cached status, retry schedule | bot/provider truth |
|
||||
| Client badge | credential receipt and installation | billing state |
|
||||
| Payment service | BBS owner commitment, proof verification, billing state, credit schedule | raw master key, credential |
|
||||
| Payment credit | product and eligible monthly slot | provider proof, credential |
|
||||
| Payment service | BBS owner commitment, proof verification, billing state, grant schedule | raw master key, credential |
|
||||
| Service grant | product and eligible monthly grant period | provider proof, credential |
|
||||
| Badge service | signing and idempotent credential cache | provider/billing logic |
|
||||
| Core | signature verification and installed badge | payment status |
|
||||
|
||||
@@ -54,43 +47,43 @@ Treat these as separate programs with typed interfaces.
|
||||
|
||||
1. Provider verification changes payment state only.
|
||||
2. Before `Prepare`, client generates one `BadgeMasterKey` with `generateMasterKey`; it is 32 random bytes and BBS message 0.
|
||||
3. Payment stores `BadgeOwnerId = SHA-256("SimpleX badge payment owner v1" || BadgeMasterKey)` and every later issue must match it.
|
||||
4. Only `CreditAvailable` plus the matching raw master key enters badge signing.
|
||||
5. Credit consumption and cached issuance result are atomic/idempotent.
|
||||
3. Payment stores `BadgeKeyCommitment = SHA-256("SimpleX badge key commitment v1" || BadgeMasterKey)` and every later issue must match it.
|
||||
4. Only `GrantReady` plus the matching raw master key enters badge signing.
|
||||
5. Grant fulfillment and cached issuance result are atomic/idempotent.
|
||||
6. Payment never activates perks; verified credential does.
|
||||
7. RPC has no caller identity or bot push. Capability authorizes each payment request.
|
||||
8. Duplicate RPCs/events return the same result. Unknown states preserve prior state.
|
||||
9. Provider dates create eligibility; retry/request time never changes badge expiry.
|
||||
|
||||
### Time and credit
|
||||
### Time and grant
|
||||
|
||||
```haskell
|
||||
data PaymentCreditState = CreditAvailable | CreditConsumed | CreditVoided
|
||||
data ServiceGrantState = GrantReady | GrantFulfilled | GrantRevoked
|
||||
|
||||
data PaymentCredit = PaymentCredit
|
||||
{ creditId :: CreditId
|
||||
data ServiceGrant = ServiceGrant
|
||||
{ grantId :: GrantId
|
||||
, paymentId :: PaymentId
|
||||
, productId :: ServiceProductId
|
||||
, slotStart :: UTCTime
|
||||
, creditState :: PaymentCreditState
|
||||
, grantPeriodStart :: UTCTime
|
||||
, grantState :: ServiceGrantState
|
||||
}
|
||||
```
|
||||
|
||||
- One-time: one credit at verified purchase time. Reject another one-time prepare while its prior one-time service period is active.
|
||||
- Subscription: `slotStart(n) = addCalendarMonths n verifiedAnchor` when `slotStart <= now < paidThrough`.
|
||||
- Monthly and yearly plans both expose one credit per eligible month.
|
||||
- Badge service computes expiry as the start of the month two months after `slotStart`.
|
||||
- Unique credit: `(payment_id, product_id, slot_start)`.
|
||||
- Example: 21 July slot → badge expires 1 September; monthly billing renews 21 August.
|
||||
- One-time: one grant at verified purchase time. Reject another one-time prepare while its prior one-time service period is active.
|
||||
- Subscription: `grantPeriodStart(n) = addCalendarMonths n verifiedAnchor` when `grantPeriodStart(n) <= now < paidThrough`.
|
||||
- Monthly and yearly plans both expose one grant per eligible month.
|
||||
- Badge service computes expiry as the start of the month two months after `grantPeriodStart`.
|
||||
- Unique grant: `(payment_id, product_id, grant_period_start)`.
|
||||
- Example: 21 July grant period → badge expires 1 September; monthly billing renews 21 August.
|
||||
|
||||
Credit eligibility by payment state:
|
||||
Grant eligibility by payment state:
|
||||
|
||||
| State | New credit |
|
||||
| State | New grant |
|
||||
|---|---|
|
||||
| `BPPaidOneTime` | its single unissued credit |
|
||||
| `BPActive` | current due slot through `paidThrough` |
|
||||
| `BPPaidOneTime` | its single unissued grant |
|
||||
| `BPSubscriptionActive` | current due grant period through `paidThrough` |
|
||||
| `BPGrace` | only while the provider explicitly reports entitlement |
|
||||
| `BPCancelAtEnd` | due slots until `paidThrough` |
|
||||
| `BPEndsAtPeriodEnd` | due grant periods until `paidThrough` |
|
||||
| all other states | none |
|
||||
|
||||

|
||||
@@ -107,12 +100,12 @@ These names are canonical. Every transition is validated against the current con
|
||||
| `CPPreparing` | prepare RPC running |
|
||||
| `CPStoreReady` | Apple/Google binding ready |
|
||||
| `CPCheckoutReady` | Stripe URL ready |
|
||||
| `CPProviderPending` | payment/approval pending |
|
||||
| `CPAwaitingPayment` | payment/approval pending |
|
||||
| `CPVerifying` | evidence/status RPC running |
|
||||
| `CPEntitled` | last bot status is paid |
|
||||
| `CPCanceling` | management/cancel operation running |
|
||||
| `CPCancelAtEnd` | renewal off; paid time remains |
|
||||
| `CPProblem` | typed error + prior snapshot + retry time |
|
||||
| `CPEndsAtPeriodEnd` | renewal off; paid time remains |
|
||||
| `CPPaymentProblem` | typed error + prior snapshot + retry time |
|
||||
| `CPExpired` | no entitlement remains |
|
||||
|
||||
### Bot payment
|
||||
@@ -121,14 +114,14 @@ These names are canonical. Every transition is validated against the current con
|
||||
|---|---|
|
||||
| `BPPrepared` | payment/capability/binding stored |
|
||||
| `BPCheckoutOpen` | Stripe Session stored |
|
||||
| `BPPendingProvider` | provider not complete |
|
||||
| `BPAwaitingPayment` | provider not complete |
|
||||
| `BPVerifying` | reconciliation lease active |
|
||||
| `BPPaidOneTime` | verified one-time payment |
|
||||
| `BPActive` | paid subscription, renewal on |
|
||||
| `BPSubscriptionActive` | paid subscription, renewal on |
|
||||
| `BPGrace` | provider grants grace |
|
||||
| `BPOnHold` | failed payment; no new credit |
|
||||
| `BPOnHold` | failed payment; no new grant |
|
||||
| `BPPaused` | provider paused entitlement |
|
||||
| `BPCancelAtEnd` | renewal off; paid time remains |
|
||||
| `BPEndsAtPeriodEnd` | renewal off; paid time remains |
|
||||
| `BPExpired` | paid time ended |
|
||||
| `BPRefunded` | verified refund/chargeback |
|
||||
| `BPRevoked` | provider revoked entitlement |
|
||||
@@ -140,7 +133,7 @@ These names are canonical. Every transition is validated against the current con
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| `CBNone` | no usable local badge |
|
||||
| `CBNeeded` | credit available |
|
||||
| `CBNeeded` | grant available |
|
||||
| `CBRequesting` | issue RPC running |
|
||||
| `CBReceived` | response cached, not installed |
|
||||
| `CBInstalling` | core verification/install running |
|
||||
@@ -152,13 +145,13 @@ These names are canonical. Every transition is validated against the current con
|
||||
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| `BBRequested` | credit/key idempotency row created |
|
||||
| `BBRequested` | grant/key idempotency row created |
|
||||
| `BBSigning` | signing lease active |
|
||||
| `BBIssued` | credential cached; credit consumed |
|
||||
| `BBIssued` | credential cached; grant fulfilled |
|
||||
| `BBRetryableFailure` | same request can retry |
|
||||
| `BBFinalFailure` | invalid/permanently unsupported request |
|
||||
|
||||
Credit states are `CreditAvailable`, `CreditConsumed`, and `CreditVoided`. There is no bot “installed” state.
|
||||
Grant states are `GrantReady`, `GrantFulfilled`, and `GrantRevoked`. There is no bot “installed” state.
|
||||
|
||||
## 3. Contracts
|
||||
|
||||
@@ -172,19 +165,19 @@ data ServiceCall = ServiceCall
|
||||
}
|
||||
|
||||
data PaymentInput
|
||||
= Prepare Provider ServiceProductId PurchaseKind BadgeOwnerId
|
||||
= Prepare Provider ServiceProductId PurchaseKind BadgeKeyCommitment
|
||||
| AppleEvidence PaymentId Capability SignedTransactionJWS
|
||||
| GoogleEvidence PaymentId Capability PurchaseToken
|
||||
| ExistingPayment PaymentId Capability
|
||||
| CancelSubscription PaymentId Capability
|
||||
| CreatePortal PaymentId Capability
|
||||
|
||||
data ServiceRequest = IssueBadge BadgeMasterKey (Maybe CreditId)
|
||||
data ServiceRequest = IssueBadge BadgeMasterKey (Maybe GrantId)
|
||||
|
||||
data ServiceResponse = ServiceResponse
|
||||
{ requestId :: RequestId
|
||||
, payment :: PaymentSnapshot
|
||||
, credit :: Maybe PaymentCreditSummary
|
||||
, grant :: Maybe ServiceGrantSummary
|
||||
, service :: Maybe (Either ServiceError BadgeCredential)
|
||||
, retryAfter :: Maybe NominalDiffTime
|
||||
}
|
||||
@@ -192,30 +185,30 @@ data ServiceResponse = ServiceResponse
|
||||
|
||||
Rules:
|
||||
|
||||
- Client calls `generateMasterKey` once before `Prepare`, persists it encrypted, computes `BadgeOwnerId`, and reuses the key for all renewal badges.
|
||||
- `Prepare` stores `BadgeOwnerId` but cannot issue a badge.
|
||||
- Client calls `generateMasterKey` once before `Prepare`, persists it encrypted, computes `BadgeKeyCommitment`, and reuses the key for all renewal badges.
|
||||
- `Prepare` stores `BadgeKeyCommitment` but cannot issue a badge.
|
||||
- Apple/Google evidence may include `IssueBadge`.
|
||||
- Stripe prepare returns Checkout data; a later `ExistingPayment + IssueBadge` issues.
|
||||
- Pending response has no credit/result and includes `retryAfter`.
|
||||
- Capability and `BadgeOwnerId` never enter Stripe metadata or a return URL.
|
||||
- `creditId` selects only; bot rechecks payment, owner, product, and eligibility.
|
||||
- Before signing, orchestrator recomputes `BadgeOwnerId` from `BadgeMasterKey`; mismatch returns `ownership_conflict` without consuming credit.
|
||||
- Pending response has no grant/result and includes `retryAfter`.
|
||||
- Capability and `BadgeKeyCommitment` never enter Stripe metadata or a return URL.
|
||||
- `grantId` selects only; bot rechecks payment, owner, product, and eligibility.
|
||||
- Before signing, orchestrator recomputes `BadgeKeyCommitment` from `BadgeMasterKey`; mismatch returns `ownership_conflict` without fulfilling grant.
|
||||
|
||||
### Internal interface
|
||||
|
||||
```haskell
|
||||
resolvePayment :: PaymentInput -> Transaction PaymentDecision
|
||||
fulfillBadge :: PaymentCredit -> BadgeRequest -> Transaction BadgeResult
|
||||
fulfillBadge :: ServiceGrant -> BadgeRequest -> Transaction BadgeResult
|
||||
```
|
||||
|
||||
Order:
|
||||
|
||||
1. authorize capability;
|
||||
2. resolve/verify payment;
|
||||
3. commit payment and create/load due credit;
|
||||
4. verify the request key matches the payment `BadgeOwnerId`;
|
||||
5. pass only credit + request to badge service;
|
||||
6. cache issuance and consume credit atomically;
|
||||
3. commit payment and create/load due grant;
|
||||
4. verify the request key matches the payment `BadgeKeyCommitment`;
|
||||
5. pass only grant + request to badge service;
|
||||
6. cache issuance and mark grant fulfilled atomically;
|
||||
7. return one final response.
|
||||
|
||||
### Idempotency and audit
|
||||
@@ -230,39 +223,31 @@ Order:
|
||||
|
||||
Product outcomes are in the Product Plan. These diagrams show implementation boundaries only.
|
||||
|
||||
### Common credit → badge path
|
||||
### Common grant → badge path
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant RPC as RPC
|
||||
participant O as Orchestrator
|
||||
participant P as Payment service
|
||||
participant X as Credit
|
||||
participant B as Badge service
|
||||
participant K as Core
|
||||
participant B as Bot
|
||||
Note over C: CPVerifying + CBRequesting
|
||||
C->>RPC: ServiceCall(payment, IssueBadge)
|
||||
RPC->>O: Authorized request
|
||||
O->>P: Resolve payment
|
||||
Note over P: BPPaidOneTime / BPActive / BPGrace / BPCancelAtEnd
|
||||
P->>X: Create or load slot
|
||||
Note over X: CreditAvailable
|
||||
O->>O: Verify BadgeMasterKey matches BadgeOwnerId
|
||||
O->>B: Create request
|
||||
Note over B: BBRequested
|
||||
O->>B: Claim signing
|
||||
Note over B: BBSigning
|
||||
B->>B: Sign and cache
|
||||
Note over B: BBIssued
|
||||
B->>X: Commit result
|
||||
Note over X: CreditConsumed
|
||||
O-->>RPC: Final status + credential
|
||||
RPC-->>C: Final response
|
||||
Note over C: CPEntitled / CPCancelAtEnd / CPProblem(BPGrace)<br/>+ CBReceived
|
||||
C->>K: Verify and install
|
||||
C->>B: RPC payment status + IssueBadge
|
||||
Note over B: Payment: BPPaidOneTime / BPSubscriptionActive / BPGrace / BPEndsAtPeriodEnd
|
||||
B->>B: Create or load eligible grant period
|
||||
Note over B: Grant: GrantReady
|
||||
B->>B: Verify BadgeMasterKey matches BadgeKeyCommitment
|
||||
B->>B: Create issuance
|
||||
Note over B: Badge: BBRequested
|
||||
B->>B: Claim signing
|
||||
Note over B: Badge: BBSigning
|
||||
B->>B: Sign and cache credential
|
||||
Note over B: Badge: BBIssued
|
||||
B->>B: Mark grant fulfilled
|
||||
Note over B: Grant: GrantFulfilled
|
||||
B-->>C: RPC payment status + credential
|
||||
Note over C: CPEntitled / CPEndsAtPeriodEnd / CPPaymentProblem(BPGrace)<br/>+ CBReceived
|
||||
C->>C: Verify credential
|
||||
Note over C: CBInstalling
|
||||
K-->>C: Installed
|
||||
C->>C: Install credential
|
||||
Note over C: CBInstalled
|
||||
```
|
||||
|
||||
@@ -271,20 +256,16 @@ sequenceDiagram
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant RPC as RPC
|
||||
participant O as Orchestrator
|
||||
participant A as Apple adapter
|
||||
participant P as Payment store
|
||||
participant B as Bot
|
||||
participant A as Apple
|
||||
Note over C: CPVerifying
|
||||
C->>RPC: Apple evidence
|
||||
RPC->>O: Authorized request
|
||||
O->>P: Claim verification lease
|
||||
Note over P: BPVerifying
|
||||
O->>A: Verify signed transaction
|
||||
A->>A: Verify signature, app, product,<br/>binding, dates, revocation
|
||||
A-->>O: Normalized result
|
||||
O->>P: Apply result
|
||||
Note over P: BPPaidOneTime or BPActive
|
||||
C->>B: RPC AppleEvidence + optional IssueBadge
|
||||
Note over B: Payment: BPVerifying
|
||||
B->>B: Verify signed transaction offline
|
||||
Note over B: Payment: BPPaidOneTime or BPSubscriptionActive
|
||||
B-->>C: RPC status + optional credential
|
||||
Note over C: CPEntitled + optional CBReceived
|
||||
Note over A: No Apple API call for initial evidence
|
||||
```
|
||||
|
||||
This path is offline. Status/restore uses App Store Server API; Notifications V2 only trigger reconciliation.
|
||||
@@ -294,58 +275,45 @@ This path is offline. Status/restore uses App Store Server API; Notifications V2
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant RPC as RPC
|
||||
participant O as Orchestrator
|
||||
participant G as Google adapter
|
||||
participant API as Publisher API
|
||||
participant P as Payment store
|
||||
participant B as Bot
|
||||
participant G as Google
|
||||
Note over C: CPVerifying
|
||||
C->>RPC: Google evidence
|
||||
RPC->>O: Authorized request
|
||||
O->>P: Claim verification lease
|
||||
Note over P: BPVerifying
|
||||
O->>G: Verify token
|
||||
G->>API: productsv2.get / subscriptionsv2.get
|
||||
API-->>G: Canonical purchase
|
||||
G-->>O: Normalized result
|
||||
O->>P: Apply result
|
||||
Note over P: BPPaidOneTime or BPActive
|
||||
C->>B: RPC GoogleEvidence + optional IssueBadge
|
||||
Note over B: Payment: BPVerifying
|
||||
B->>G: Verify purchase token
|
||||
G-->>B: Canonical purchase
|
||||
Note over B: Payment: BPPaidOneTime or BPSubscriptionActive
|
||||
B-->>C: RPC status + optional credential
|
||||
Note over C: CPEntitled + optional CBReceived
|
||||
```
|
||||
|
||||
Commit entitlement before outbox acknowledgement/consume. RTDN triggers provider GET; never grant from notification payload.
|
||||
Commit entitlement before outbox acknowledgement/consume. RTDN triggers provider GET; never create a service grant from the notification payload.
|
||||
|
||||
### Stripe Checkout and webhook
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant RPC as RPC
|
||||
participant P as Payment service
|
||||
participant X as Credit
|
||||
participant B as Bot
|
||||
participant S as Stripe
|
||||
participant W as Webhook endpoint
|
||||
Note over C: CPPreparing
|
||||
C->>RPC: Prepare Stripe
|
||||
RPC->>P: Authorized request
|
||||
Note over P: BPPrepared
|
||||
P->>S: Create Checkout Session
|
||||
S-->>P: Session ID + URL
|
||||
Note over P: BPCheckoutOpen
|
||||
P-->>RPC: payment ID + capability + URL
|
||||
RPC-->>C: Final response
|
||||
C->>B: RPC Prepare Stripe
|
||||
Note over B: Payment: BPPrepared
|
||||
B->>S: Create Checkout Session
|
||||
S-->>B: Session ID + URL
|
||||
Note over B: Payment: BPCheckoutOpen
|
||||
B-->>C: RPC payment ID + capability + URL
|
||||
Note over C: CPCheckoutReady
|
||||
C->>C: Open Checkout and start polling
|
||||
Note over C: CPProviderPending
|
||||
S-->>W: Signed webhook
|
||||
W->>W: Verify and persist event
|
||||
W-->>S: 2xx
|
||||
W->>P: Reconcile event
|
||||
Note over P: BPVerifying
|
||||
P->>S: Retrieve current objects
|
||||
S-->>P: Canonical payment
|
||||
Note over P: BPPaidOneTime or BPActive
|
||||
P->>X: Create or load due slot
|
||||
Note over X: CreditAvailable
|
||||
C->>S: Open Checkout
|
||||
Note over C: CPAwaitingPayment
|
||||
S-->>B: Signed webhook
|
||||
Note over B: Payment: BPVerifying
|
||||
B->>S: Retrieve current payment
|
||||
S-->>B: Canonical payment
|
||||
Note over B: Payment: BPPaidOneTime or BPSubscriptionActive
|
||||
B->>B: Create or load due grant period
|
||||
Note over B: Grant: GrantReady
|
||||
Note over C: Still CPAwaitingPayment, no bot push
|
||||
```
|
||||
|
||||
Webhook handler verifies the raw body, persists/deduplicates event ID, returns `2xx`, then workers reconcile. Client remains pending until its next RPC.
|
||||
@@ -355,19 +323,16 @@ Webhook handler verifies the raw body, persists/deduplicates event ID, returns `
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant RPC as RPC
|
||||
participant P as Payment service
|
||||
participant B as Bot
|
||||
participant S as Stripe
|
||||
Note over C: CPVerifying
|
||||
C->>RPC: ExistingPayment
|
||||
RPC->>P: Authorized request
|
||||
Note over P: BPVerifying
|
||||
P->>S: Retrieve Checkout
|
||||
S-->>P: Pending / unpaid
|
||||
Note over P: BPPendingProvider
|
||||
P-->>RPC: Pending + retryAfter
|
||||
RPC-->>C: Final response
|
||||
Note over C: CPProviderPending
|
||||
C->>B: RPC ExistingPayment
|
||||
Note over B: Payment: BPVerifying
|
||||
B->>S: Retrieve Checkout
|
||||
S-->>B: Pending / unpaid
|
||||
Note over B: Payment: BPAwaitingPayment
|
||||
B-->>C: RPC pending + retryAfter
|
||||
Note over C: CPAwaitingPayment
|
||||
```
|
||||
|
||||
Poll from Checkout open and on return/foreground at 5, 15, 30, 60, 120 seconds. Then use normal reconciliation. Deep links are optional; no localhost listener.
|
||||
@@ -376,9 +341,9 @@ Poll from Checkout open and on return/foreground at 5, 15, 30, 60, 120 seconds.
|
||||
|
||||
| Provider | Client action | Bot action | Confirmed state |
|
||||
|---|---|---|---|
|
||||
| Apple | open Apple management UI; status RPC on return | App Store Server API status | `BPCancelAtEnd` |
|
||||
| Google | open Play management UI; status RPC on return | `subscriptionsv2.get` | `BPCancelAtEnd` |
|
||||
| Stripe | `CancelSubscription` RPC | set `cancel_at_period_end=true`, retrieve Subscription | `BPCancelAtEnd` |
|
||||
| Apple | open Apple management UI; status RPC on return | App Store Server API status | `BPEndsAtPeriodEnd` |
|
||||
| Google | open Play management UI; status RPC on return | `subscriptionsv2.get` | `BPEndsAtPeriodEnd` |
|
||||
| Stripe | `CancelSubscription` RPC | set `cancel_at_period_end=true`, retrieve Subscription | `BPEndsAtPeriodEnd` |
|
||||
|
||||
Failure preserves previous state; client shows Retry and still says **Renews on**. “Already canceled” is success. Stripe Portal cancellation is disabled.
|
||||
|
||||
@@ -397,23 +362,23 @@ Mirror existing `data CallState` machinery:
|
||||
|
||||
References: `Simplex.Chat.Call`, `Store.Profiles`, `Library.Commands`, `Library.Subscriber`, and `Controller`.
|
||||
|
||||
Define five separate sums: client payment, client badge, bot payment, credit, bot badge. Do not encode state as one nullable record.
|
||||
Define five separate sums: client payment, client badge, bot payment, grant, bot badge. Do not encode state as one nullable record.
|
||||
|
||||
### Client tables
|
||||
|
||||
`badge_payments`: provider/product/plan, payment state payload, encrypted capability and `BadgeMasterKey`, `BadgeOwnerId`, binding/proof reference, `paidThrough`, `willRenew`, checked/retry time, version.
|
||||
`badge_payments`: provider/product/plan, payment state payload, encrypted capability and `BadgeMasterKey`, `BadgeKeyCommitment`, binding/proof reference, `paidThrough`, `willRenew`, checked/retry time, version.
|
||||
|
||||
`badges`: payment/credit/slot/key hash, badge state payload, cached credential, expiry, attempt/error, version.
|
||||
`badges`: payment/grant/grant-period/key hash, badge state payload, cached credential, expiry, attempt/error, version.
|
||||
|
||||
Join by payment/credit ID only. Update active profile only after core installation.
|
||||
Join by payment/grant ID only. Update active profile only after core installation.
|
||||
|
||||
### Bot tables
|
||||
|
||||
| Table | Unique key / purpose |
|
||||
|---|---|
|
||||
| `payments` | provider-object ownership + `BadgeOwnerId`; canonical payment sum |
|
||||
| `payment_credits` | payment + product + slot |
|
||||
| `badge_issuances` | credit + master-key hash; cached credential |
|
||||
| `payments` | provider-object ownership + `BadgeKeyCommitment`; canonical payment sum |
|
||||
| `service_grants` | payment + product + grant period |
|
||||
| `badge_issuances` | grant + master-key hash; cached credential |
|
||||
| `rpc_requests` | request ID; request hash + final response |
|
||||
| `provider_events` | provider event ID; dedupe/result |
|
||||
| `outbox` | acknowledge, consume, reconciliation, cleanup |
|
||||
@@ -432,7 +397,7 @@ reconcile(paymentId):
|
||||
render cached payment + installed badge
|
||||
submit unseen Apple/Google evidence
|
||||
request status for nonterminal payment
|
||||
if CreditAvailable and no badge covers its slot: CBNeeded -> request IssueBadge
|
||||
if GrantReady and no badge covers its grant period: CBNeeded -> request IssueBadge
|
||||
if credential returned: CBReceived -> CBInstalling -> CBInstalled
|
||||
schedule next check
|
||||
```
|
||||
@@ -450,24 +415,24 @@ Every input is one of:
|
||||
|
||||
| Input/result | Class | Client | Bot |
|
||||
|---|---|---|---|
|
||||
| payment pending | retry | `CPProviderPending`; schedule | `BPPendingProvider`; no credit |
|
||||
| timeout/429/5xx | retry | `CPProblem` with prior snapshot | preserve current `BP…`; return `retryAfter` |
|
||||
| payment pending | retry | `CPAwaitingPayment`; schedule | `BPAwaitingPayment`; no grant |
|
||||
| timeout/429/5xx | retry | `CPPaymentProblem` with prior snapshot | preserve current `BP…`; return `retryAfter` |
|
||||
| lost response | retry | preserve state; repeat same ID/body | return cached result/state |
|
||||
| duplicate event/request | idempotent | accept same state/result | preserve state; dedupe/re-fetch |
|
||||
| ID reused with new body | reject | preserve state; new ID only for new action | preserve state; telemetry |
|
||||
| invalid capability/binding | reject | preserve state; restore/support | preserve state; rate-limit |
|
||||
| invalid proof/product | reject | `CPProblem`; no blind retry | preserve `BP…`; quarantine/alert |
|
||||
| unknown provider state | quarantine | `CPProblem`; retry later | preserve `BP…`; re-fetch, never guess |
|
||||
| `CreditAvailable` | apply | `CBNeeded` → `CBRequesting` | `BBRequested` when requested |
|
||||
| `CreditConsumed` | idempotent | `CBReceived` → install | return cached `BBIssued` |
|
||||
| signing unavailable | retry | `CBRetryableFailure`; keep old badge | `BBRetryableFailure`; credit stays available |
|
||||
| invalid key/credential/protocol | reject | `CBFinalFailure` | `BBFinalFailure`; do not consume credit |
|
||||
| invalid proof/product | reject | `CPPaymentProblem`; no blind retry | preserve `BP…`; quarantine/alert |
|
||||
| unknown provider state | quarantine | `CPPaymentProblem`; retry later | preserve `BP…`; re-fetch, never guess |
|
||||
| `GrantReady` | apply | `CBNeeded` → `CBRequesting` | `BBRequested` when requested |
|
||||
| `GrantFulfilled` | idempotent | `CBReceived` → install | return cached `BBIssued` |
|
||||
| signing unavailable | retry | `CBRetryableFailure`; keep old badge | `BBRetryableFailure`; grant stays available |
|
||||
| invalid key/credential/protocol | reject | `CBFinalFailure` | `BBFinalFailure`; do not fulfill grant |
|
||||
| install crash | local retry | resume `CBReceived` → `CBInstalling` | no bot transition |
|
||||
| cancel timeout | retry | `CPProblem`; still show Renews | preserve `BPActive`/`BPCancelAtEnd` |
|
||||
| already canceled | idempotent | `CPCancelAtEnd` | return `BPCancelAtEnd` |
|
||||
| cancel timeout | retry | `CPPaymentProblem`; still show Renews | preserve `BPSubscriptionActive`/`BPEndsAtPeriodEnd` |
|
||||
| already canceled | idempotent | `CPEndsAtPeriodEnd` | return `BPEndsAtPeriodEnd` |
|
||||
| user cancels store | exit | restore prior `CP…`/`CB…` | `BPPrepared` expires later |
|
||||
| Stripe Checkout expired | final attempt | `CPExpired`; new checkout on user action | `BPExpired`; no credit |
|
||||
| refund/revocation | apply | `CPExpired`; signed badge survives to expiry | `BPRefunded`/`BPRevoked`; void unused credits |
|
||||
| Stripe Checkout expired | final attempt | `CPExpired`; new checkout on user action | `BPExpired`; no grant |
|
||||
| refund/revocation | apply | `CPExpired`; signed badge survives to expiry | `BPRefunded`/`BPRevoked`; mark unused grants `GrantRevoked` |
|
||||
| webhook DB failure | retry delivery | no transition | no transition; non-2xx |
|
||||
|
||||
Stable codes: `bad_request`, `unsupported_version`, `payment_pending`, `payment_not_entitled`, `ownership_conflict`, `proof_invalid`, `provider_rate_limited`, `provider_unavailable`, `idempotency_mismatch`, `badge_already_issued`, `signing_failed`, `internal_error`.
|
||||
@@ -476,7 +441,7 @@ Stable codes: `bad_request`, `unsupported_version`, `payment_pending`, `payment_
|
||||
|
||||
- Before provider call: repeat request.
|
||||
- Provider succeeds before commit: retrieve by idempotency key/object binding.
|
||||
- Payment committed before issuance: `CreditAvailable` remains unchanged.
|
||||
- Payment committed before issuance: `GrantReady` remains unchanged.
|
||||
- Credential cached before response loss: repeat returns it.
|
||||
- Response cached before install: resume local installation.
|
||||
- Duplicate/out-of-order event: dedupe, re-fetch, monotonic transition.
|
||||
@@ -493,17 +458,17 @@ Provider-state mapping:
|
||||
|
||||
| Provider state | Canonical bot state |
|
||||
|---|---|
|
||||
| Apple active | `BPActive` |
|
||||
| Apple active | `BPSubscriptionActive` |
|
||||
| Apple grace | `BPGrace` while Apple reports entitlement |
|
||||
| Apple billing retry without entitlement | `BPOnHold` |
|
||||
| Apple renewal off / expired / refund / revoke | `BPCancelAtEnd` / `BPExpired` / `BPRefunded` / `BPRevoked` |
|
||||
| Google pending / active / grace | `BPPendingProvider` / `BPActive` / `BPGrace` |
|
||||
| Apple renewal off / expired / refund / revoke | `BPEndsAtPeriodEnd` / `BPExpired` / `BPRefunded` / `BPRevoked` |
|
||||
| Google pending / active / grace | `BPAwaitingPayment` / `BPSubscriptionActive` / `BPGrace` |
|
||||
| Google on-hold / paused | `BPOnHold` / `BPPaused` |
|
||||
| Google canceled with time remaining / expired | `BPCancelAtEnd` / `BPExpired` |
|
||||
| Stripe Checkout open or async pending | `BPCheckoutOpen` / `BPPendingProvider` |
|
||||
| Stripe paid one-time / paid subscription invoice | `BPPaidOneTime` / `BPActive` |
|
||||
| Google canceled with time remaining / expired | `BPEndsAtPeriodEnd` / `BPExpired` |
|
||||
| Stripe Checkout open or async pending | `BPCheckoutOpen` / `BPAwaitingPayment` |
|
||||
| Stripe paid one-time / paid subscription invoice | `BPPaidOneTime` / `BPSubscriptionActive` |
|
||||
| Stripe past-due, unpaid, or paused | `BPOnHold` / `BPPaused` according to retrieved status |
|
||||
| Stripe cancel-at-end / deleted | `BPCancelAtEnd` / `BPExpired` |
|
||||
| Stripe cancel-at-end / deleted | `BPEndsAtPeriodEnd` / `BPExpired` |
|
||||
| Stripe refund / dispute | `BPRefunded` / `BPRevoked` |
|
||||
|
||||
Google linked-token replacement changes subscription identity/period data, then maps the retrieved state using this table.
|
||||
@@ -512,7 +477,7 @@ Rules:
|
||||
|
||||
- Google initial subscription acknowledgement and one-time consumption run from durable outbox.
|
||||
- Stripe uses server-selected Price, mode, Customer, `client_reference_id=paymentId`, metadata, and redirect URLs.
|
||||
- Stripe subscription credit requires a paid invoice, not merely active Subscription status.
|
||||
- Stripe subscription grant requires a paid invoice, not merely active Subscription status.
|
||||
- Webhook/status/completion page use one reconciliation function; redirects never fulfill.
|
||||
- Portal is for invoices/payment methods only. Apple/Google normal cancellation is store UI.
|
||||
|
||||
@@ -520,7 +485,7 @@ Rules:
|
||||
|
||||
- Verify provider signatures/objects server-side; never trust decoded client/redirect fields.
|
||||
- Hash capabilities; encrypt retained proofs/provider IDs; rotate keys.
|
||||
- Keep raw `BadgeMasterKey` client-encrypted and bot-memory-only during ownership verification/signing; persist only domain-separated `BadgeOwnerId`.
|
||||
- Keep raw `BadgeMasterKey` client-encrypted and bot-memory-only during ownership verification/signing; persist only domain-separated `BadgeKeyCommitment`.
|
||||
- Allowlist product, app/package, environment, currency/price, and account binding.
|
||||
- Rate-limit operation/payment and cap payload sizes.
|
||||
- Serialize payment mutations with lock/version; events and RPC use the same transitions.
|
||||
@@ -529,7 +494,7 @@ Rules:
|
||||
|
||||
## 9. Delivery and tests
|
||||
|
||||
1. **Schema/protocol:** five sums/codecs, migrations, credit boundary, request ledger, Chat Console audit, core install API.
|
||||
1. **Schema/protocol:** five sums/codecs, migrations, grant boundary, request ledger, Chat Console audit, core install API.
|
||||
2. **Apple/Google:** bindings, verification/status, Notifications V2/RTDN, acknowledge/consume, native UI.
|
||||
3. **Stripe:** Checkout, completion page, webhook, reconciliation, cancel RPC, restricted Portal.
|
||||
4. **UX/hardening:** scheduler, all Product states, rollout compatibility, telemetry, cleanup.
|
||||
@@ -541,9 +506,9 @@ Tests:
|
||||
- message tests proving only the named owner changes state;
|
||||
- Apple JWS/status/notification and Google pending/renewal/grace/hold/cancel cases;
|
||||
- Stripe async payment, invoice renewal, cancellation, closed app/browser, delayed/duplicate/reordered webhook;
|
||||
- monthly/yearly slots and 21 July → 31 August expiry;
|
||||
- monthly/yearly grant periods and 21 July → 31 August expiry;
|
||||
- crash/replay at every side-effect boundary;
|
||||
- capability/credit/BBS-owner isolation and wrong-`BadgeMasterKey` rejection;
|
||||
- capability/grant/BBS-owner isolation and wrong-`BadgeMasterKey` rejection;
|
||||
- Chat Console coverage and redaction snapshots.
|
||||
|
||||
Release gates: provider sandbox E2E, webhook signature/replay, schema rollback, store-policy review, complete error handling, operational dashboards.
|
||||
@@ -557,8 +522,8 @@ Release gates: provider sandbox E2E, webhook signature/replay, schema rollback,
|
||||
| RPC/controller/console | calls, response handling, redacted audit |
|
||||
| client store/migrations | separate payment and badge stores |
|
||||
| Kotlin/Swift | derive Product UX state |
|
||||
| bot payment repository | replace `customData`; providers, credits, outbox |
|
||||
| bot badge repository | credit-only signing/cache; no provider imports |
|
||||
| bot payment repository | replace `customData`; providers, grants, outbox |
|
||||
| bot badge repository | grant-only signing/cache; no provider imports |
|
||||
| `badge-service/apple.py` | proof + subscription status |
|
||||
| `badge-service/google.py` | full mapping + acknowledge/consume |
|
||||
| `badge-service/stripe_api.py` | Checkout/webhook/status/cancel/Portal |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
**Status:** implementation-ready
|
||||
**Companion:** [Implementation plan](2026-07-20-supporter-badges-v2-implementation.md)
|
||||
|
||||
Payment and badge are separate: payment creates a service credit for an eligible monthly slot; the credit issues one badge; core verifies and installs it.
|
||||
Payment and badge are separate: payment creates a service grant for an eligible monthly grant period; the grant authorizes one badge issuance; core verifies and installs it.
|
||||
|
||||

|
||||
|
||||
@@ -31,7 +31,7 @@ Choices: **One-time**, **Monthly**, **Yearly**. There is no Extend action.
|
||||
|
||||
- One-time does not stack and is available again after badge expiry.
|
||||
- Subscribing from one-time starts a new payment; there is no conversion API.
|
||||
- Monthly and yearly plans issue one badge credit per eligible month.
|
||||
- Monthly and yearly plans issue one service grant per eligible month.
|
||||
- Cancellation stops renewal. It does not shorten an issued badge.
|
||||
- Stripe uses the system browser. No localhost service is required.
|
||||
|
||||
@@ -40,7 +40,7 @@ Choices: **One-time**, **Monthly**, **Yearly**. There is no Extend action.
|
||||
| Payment event | Billing | Badge |
|
||||
|---|---|---|
|
||||
| Paid 21 July | monthly renews 21 August; yearly renews 21 July next year | valid through 31 August |
|
||||
| Eligible slot 21 August | monthly renews 21 September; yearly billing unchanged | new badge valid through 30 September |
|
||||
| Eligible grant period 21 August | monthly renews 21 September; yearly billing unchanged | new badge valid through 30 September |
|
||||
| Canceled before renewal | subscription remains paid to provider period end | issued badge remains valid to signed expiry |
|
||||
|
||||
Show **Badge valid until** separately from **Renews on** or **Subscription ends on**.
|
||||
@@ -48,7 +48,7 @@ Show **Badge valid until** separately from **Renews on** or **Subscription ends
|
||||
### Sources of truth
|
||||
|
||||
- Core signature and expiry decide badge validity.
|
||||
- Bot/provider verification decides payment status and credit eligibility.
|
||||
- Bot/provider verification decides payment status and grant eligibility.
|
||||
- Store state and Stripe redirects are hints only.
|
||||
- Client asks through RPC; bot returns one final response and never pushes.
|
||||
- Before payment, the client generates one 32-byte BBS `BadgeMasterKey`. The payment and every badge issued from it are bound to that key; renewals reuse it.
|
||||
@@ -65,7 +65,7 @@ Show **Badge valid until** separately from **Renews on** or **Subscription ends
|
||||
| Active subscription | paid, renewing, badge active | interval; badge expiry; renewal | Cancel; Manage |
|
||||
| Canceled, active | renewal off; paid/badge time remains | badge expiry; subscription end | Resubscribe |
|
||||
| Payment issue | grace/on-hold/provider error | active badge until expiry | Fix payment; Check again |
|
||||
| Badge missing | credit exists; no usable badge | issuance error/progress | Retry |
|
||||
| Badge missing | grant exists; no usable badge | issuance error/progress | Retry |
|
||||
| Expired | no entitlement or active badge | expired state | Buy once; Monthly; Yearly |
|
||||
| Needs update | unknown issuer/protocol | unavailable | Update app |
|
||||
| Offline/stale | refresh failed | cached state + check time | Retry |
|
||||
@@ -127,7 +127,7 @@ sequenceDiagram
|
||||
C->>A: Purchase
|
||||
A-->>C: Pending
|
||||
Note over C: Payment pending, badge unchanged
|
||||
Note over B: Prepared, no credit
|
||||
Note over B: Prepared, no grant
|
||||
```
|
||||
|
||||
#### Canceled
|
||||
@@ -181,7 +181,7 @@ sequenceDiagram
|
||||
C->>G: Purchase
|
||||
G-->>C: Pending
|
||||
Note over C: Payment pending, badge unchanged
|
||||
Note over B: Prepared, no credit
|
||||
Note over B: Prepared, no grant
|
||||
```
|
||||
|
||||
#### Canceled
|
||||
@@ -214,7 +214,7 @@ sequenceDiagram
|
||||
C->>S: Open Checkout
|
||||
Note over C: Payment pending, start polling
|
||||
S-->>B: Signed webhook
|
||||
Note over B: Payment entitled, credit available
|
||||
Note over B: Payment entitled, grant available
|
||||
C->>B: Status + badge request
|
||||
B-->>C: Status + badge
|
||||
Note over C: Entitled, badge installed
|
||||
@@ -230,7 +230,7 @@ sequenceDiagram
|
||||
C->>B: Status request
|
||||
B->>S: Retrieve Checkout
|
||||
S-->>B: Pending
|
||||
Note over B: Pending, no credit
|
||||
Note over B: Pending, no grant
|
||||
B-->>C: Pending + retry time
|
||||
Note over C: Poll later, badge unchanged
|
||||
```
|
||||
@@ -243,7 +243,7 @@ sequenceDiagram
|
||||
participant B as Bot
|
||||
participant S as Stripe
|
||||
S-->>B: Checkout expired
|
||||
Note over B: Expired, no credit
|
||||
Note over B: Expired, no grant
|
||||
C->>B: Status request
|
||||
B-->>C: Checkout expired
|
||||
Note over C: New checkout requires user action
|
||||
@@ -307,7 +307,7 @@ Never show canceled until the bot confirms renewal is off.
|
||||
|
||||
Refresh on launch, foreground, profile switch, network restore, store update, Stripe browser return, manual retry, six-hour jittered timer, and payment/badge date boundaries.
|
||||
|
||||
If paid credit exists without the current badge, request issuance. Cache the response before core verification/install. There are no bot-initiated client events.
|
||||
If `GrantReady` exists without the current badge, request issuance. Cache the response before core verification/install. There are no bot-initiated client events.
|
||||
|
||||
| Condition | Client action |
|
||||
|---|---|
|
||||
@@ -330,7 +330,7 @@ Errors preserve the last payment snapshot and installed badge. The implementatio
|
||||
- Choices are One-time, Monthly, Yearly; no Extend.
|
||||
- Payment on 21 July shows badge through 31 August while billing keeps its provider date.
|
||||
- Apple, Google, and Stripe have separate linear outcomes.
|
||||
- Payment verification creates provider-neutral credit; badge service has no provider logic.
|
||||
- Payment verification creates a provider-neutral service grant; badge service has no provider logic.
|
||||
- Client and bot payment/badge states are separate.
|
||||
- RPC is client-request/bot-response only and idempotent.
|
||||
- Stripe needs no localhost/deep-link success and cancels through bot RPC.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="430" viewBox="0 0 1100 430"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.credit{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1100" height="430" fill="#f8fafc"/><text x="550" y="36" text-anchor="middle" class="title">End-to-end lifecycle — ownership at each step</text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="430" viewBox="0 0 1100 430"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.grant{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1100" height="430" fill="#f8fafc"/><text x="550" y="36" text-anchor="middle" class="title">End-to-end lifecycle — ownership at each step</text>
|
||||
<text x="28" y="91" class="h">CLIENT</text><rect class="box client" rx="10" x="125" y="60" width="180" height="70"/><text x="215" y="88" text-anchor="middle" class="h">Payment state</text><text x="215" y="111" text-anchor="middle" class="b">prepare · pending · refresh</text><rect class="box client" rx="10" x="795" y="60" width="150" height="70"/><text x="870" y="88" text-anchor="middle" class="h">Badge state</text><text x="870" y="111" text-anchor="middle" class="b">receive · install</text>
|
||||
<text x="28" y="221" class="h">BOT</text><rect class="box pay" rx="10" x="125" y="180" width="180" height="80"/><text x="215" y="210" text-anchor="middle" class="h">Payment service</text><text x="215" y="233" text-anchor="middle" class="b">verify provider entitlement</text><rect class="box credit" rx="10" x="405" y="180" width="180" height="80"/><text x="495" y="210" text-anchor="middle" class="h">PaymentCredit</text><text x="495" y="233" text-anchor="middle" class="b">available · consumed · voided</text><rect class="box badge" rx="10" x="685" y="180" width="180" height="80"/><text x="775" y="210" text-anchor="middle" class="h">Badge service</text><text x="775" y="233" text-anchor="middle" class="b">request · sign · cache</text>
|
||||
<text x="28" y="221" class="h">BOT</text><rect class="box pay" rx="10" x="125" y="180" width="180" height="80"/><text x="215" y="210" text-anchor="middle" class="h">Payment service</text><text x="215" y="233" text-anchor="middle" class="b">verify provider entitlement</text><rect class="box grant" rx="10" x="405" y="180" width="180" height="80"/><text x="495" y="210" text-anchor="middle" class="h">ServiceGrant</text><text x="495" y="233" text-anchor="middle" class="b">ready · fulfilled · revoked</text><rect class="box badge" rx="10" x="685" y="180" width="180" height="80"/><text x="775" y="210" text-anchor="middle" class="h">Badge service</text><text x="775" y="233" text-anchor="middle" class="b">request · sign · cache</text>
|
||||
<text x="28" y="349" class="h">PROVIDER</text><rect class="box provider" rx="10" x="125" y="310" width="180" height="70"/><text x="215" y="338" text-anchor="middle" class="h">Apple / Google / Stripe</text><text x="215" y="361" text-anchor="middle" class="b">charge · renew · refund</text>
|
||||
<path class="line" d="M215 130 V180"/><text x="225" y="158" class="s">RPC evidence/status</text><path class="line" d="M215 310 V260"/><text x="225" y="291" class="s">API / webhook</text><path class="line" d="M305 220 H405"/><text x="355" y="207" text-anchor="middle" class="s">verified slot</text><path class="line" d="M585 220 H685"/><text x="635" y="207" text-anchor="middle" class="s">internal contract</text><path class="line" d="M775 180 V132 H795"/><text x="780" y="151" class="s">RPC result</text><path class="line" d="M945 95 H1015 V220 H865"/><text x="1000" y="154" text-anchor="middle" class="s">next refresh</text>
|
||||
<path class="line" d="M215 130 V180"/><text x="225" y="158" class="s">RPC evidence/status</text><path class="line" d="M215 310 V260"/><text x="225" y="291" class="s">API / webhook</text><path class="line" d="M305 220 H405"/><text x="355" y="207" text-anchor="middle" class="s">verified grant period</text><path class="line" d="M585 220 H685"/><text x="635" y="207" text-anchor="middle" class="s">internal contract</text><path class="line" d="M775 180 V132 H795"/><text x="780" y="151" class="s">RPC result</text><path class="line" d="M945 95 H1015 V220 H865"/><text x="1000" y="154" text-anchor="middle" class="s">next refresh</text>
|
||||
<text x="550" y="414" text-anchor="middle" class="muted b">Payment never installs a badge. Badge issuance never interprets provider state.</text></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="500" viewBox="0 0 1100 500"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.credit{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1100" height="500" fill="#f8fafc"/><text x="550" y="36" text-anchor="middle" class="title">Responsibility and trust boundaries</text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="500" viewBox="0 0 1100 500"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.grant{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1100" height="500" fill="#f8fafc"/><text x="550" y="36" text-anchor="middle" class="title">Responsibility and trust boundaries</text>
|
||||
<rect class="box client" rx="10" x="35" y="75" width="230" height="300"/><text x="150" y="106" text-anchor="middle" class="h">CLIENT ORCHESTRATOR</text><text x="55" y="143" class="b">• separate payment + badge machines</text><text x="55" y="172" class="b">• native store / hosted browser UI</text><text x="55" y="201" class="b">• owns capability + master key</text><text x="55" y="230" class="b">• asks through one-off RPC</text><text x="55" y="259" class="b">• core verifies and installs badge</text><text x="55" y="288" class="b">• reconciliation and local audit</text>
|
||||
<rect class="box pay" rx="10" x="330" y="75" width="210" height="220"/><text x="435" y="106" text-anchor="middle" class="h">PAYMENT SERVICE</text><text x="350" y="143" class="b">• capability authorization</text><text x="350" y="172" class="b">• provider verification/status</text><text x="350" y="201" class="b">• billing and credit schedule</text><text x="350" y="230" class="b">• cancellation where supported</text><text x="350" y="259" class="b">• never sees badge master key</text>
|
||||
<rect class="box credit" rx="10" x="595" y="120" width="180" height="130"/><text x="685" y="151" text-anchor="middle" class="h">PAYMENT CREDIT</text><text x="615" y="188" class="b">• provider-neutral product</text><text x="615" y="217" class="b">• one monthly service slot</text>
|
||||
<rect class="box badge" rx="10" x="830" y="75" width="235" height="220"/><text x="947" y="106" text-anchor="middle" class="h">BADGE SERVICE</text><text x="850" y="143" class="b">• accepts credit + badge request</text><text x="850" y="172" class="b">• signs and caches credential</text><text x="850" y="201" class="b">• consumes credit atomically</text><text x="850" y="230" class="b">• never imports provider logic</text><text x="850" y="259" class="b">• never changes payment state</text>
|
||||
<rect class="box pay" rx="10" x="330" y="75" width="210" height="220"/><text x="435" y="106" text-anchor="middle" class="h">PAYMENT SERVICE</text><text x="350" y="143" class="b">• capability authorization</text><text x="350" y="172" class="b">• provider verification/status</text><text x="350" y="201" class="b">• billing and grant schedule</text><text x="350" y="230" class="b">• cancellation where supported</text><text x="350" y="259" class="b">• never sees badge master key</text>
|
||||
<rect class="box grant" rx="10" x="595" y="120" width="180" height="130"/><text x="685" y="151" text-anchor="middle" class="h">SERVICE GRANT</text><text x="615" y="188" class="b">• provider-neutral authorization</text><text x="615" y="217" class="b">• one monthly grant period</text>
|
||||
<rect class="box badge" rx="10" x="830" y="75" width="235" height="220"/><text x="947" y="106" text-anchor="middle" class="h">BADGE SERVICE</text><text x="850" y="143" class="b">• accepts grant + badge request</text><text x="850" y="172" class="b">• signs and caches credential</text><text x="850" y="201" class="b">• fulfills grant atomically</text><text x="850" y="230" class="b">• never imports provider logic</text><text x="850" y="259" class="b">• never changes payment state</text>
|
||||
<rect class="box provider" rx="10" x="330" y="350" width="210" height="90"/><text x="435" y="381" text-anchor="middle" class="h">PROVIDER ADAPTERS</text><text x="435" y="407" text-anchor="middle" class="b">Apple · Google · Stripe</text>
|
||||
<path class="line" d="M265 185 H330"/><text x="297" y="171" text-anchor="middle" class="s">RPC</text><path class="line" d="M540 185 H595"/><path class="line" d="M775 185 H830"/><path class="line" d="M435 350 V295"/><text x="448" y="329" class="s">typed API</text><path class="dash" d="M947 295 V330 H150 V375"/><text x="687" y="320" text-anchor="middle" class="s">final credential response; installation remains client-owned</text>
|
||||
<text x="550" y="480" text-anchor="middle" class="muted b">Only verified payment creates credit; only core verification activates a badge.</text></svg>
|
||||
<text x="550" y="480" text-anchor="middle" class="muted b">Only verified payment creates a grant; only core verification activates a badge.</text></svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="620" viewBox="0 0 1200 620"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.credit{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1200" height="620" fill="#f8fafc"/><text x="600" y="35" text-anchor="middle" class="title">Five separate state machines</text>
|
||||
<text x="25" y="103" class="h">CLIENT PAYMENT</text><rect class="box client" rx="8" x="165" y="65" width="1000" height="65"/><text x="185" y="91" class="b">CPNone → CPPreparing → CPStoreReady / CPCheckoutReady → CPProviderPending → CPVerifying → CPEntitled</text><text x="185" y="115" class="b">Branches: CPCanceling → CPCancelAtEnd · CPProblem → retry · CPExpired → new purchase</text>
|
||||
<text x="25" y="203" class="h">BOT PAYMENT</text><rect class="box pay" rx="8" x="165" y="155" width="1000" height="85"/><text x="185" y="181" class="b">BPPrepared → BPCheckoutOpen / BPPendingProvider → BPVerifying → BPPaidOneTime / BPActive</text><text x="185" y="205" class="b">Provider branches: BPGrace · BPOnHold · BPPaused · BPCancelAtEnd · BPExpired · BPRefunded · BPRevoked</text><text x="185" y="226" class="s">Provider messages and webhooks change this lane only.</text>
|
||||
<text x="25" y="303" class="h">PAYMENT CREDIT</text><rect class="box credit" rx="8" x="165" y="265" width="1000" height="65"/><text x="185" y="292" class="b">CreditAvailable → CreditConsumed</text><text x="520" y="292" class="b">or CreditAvailable → CreditVoided</text><text x="185" y="315" class="s">The only payment-to-service interface; unique by payment + product + monthly slot.</text>
|
||||
<text x="25" y="403" class="h">BOT BADGE</text><rect class="box badge" rx="8" x="165" y="355" width="1000" height="85"/><text x="185" y="382" class="b">BBRequested → BBSigning → BBIssued</text><text x="520" y="382" class="b">or BBRetryableFailure / BBFinalFailure</text><text x="185" y="407" class="s">Consumes credit only when the credential is durably cached.</text><text x="185" y="427" class="s">It has no installed state and does not read provider/payment state.</text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="620" viewBox="0 0 1200 620"><style>text{font-family:Inter,Arial,sans-serif;fill:#172033}.title{font-size:22px;font-weight:700}.h{font-size:15px;font-weight:700}.b{font-size:12px}.s{font-size:11px}.muted{fill:#596579}.box{fill:#fff;stroke:#94a3b8;stroke-width:1.5}.client{fill:#eff6ff;stroke:#3b82f6}.pay{fill:#fffbeb;stroke:#f59e0b}.grant{fill:#f5f3ff;stroke:#8b5cf6}.badge{fill:#ecfdf5;stroke:#10b981}.provider{fill:#fff1f2;stroke:#f43f5e}.line{stroke:#64748b;stroke-width:2;fill:none;marker-end:url(#a)}.dash{stroke:#64748b;stroke-width:1.5;stroke-dasharray:5 4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><rect width="1200" height="620" fill="#f8fafc"/><text x="600" y="35" text-anchor="middle" class="title">Five separate state machines</text>
|
||||
<text x="25" y="103" class="h">CLIENT PAYMENT</text><rect class="box client" rx="8" x="165" y="65" width="1000" height="65"/><text x="185" y="91" class="b">CPNone → CPPreparing → CPStoreReady / CPCheckoutReady → CPAwaitingPayment → CPVerifying → CPEntitled</text><text x="185" y="115" class="b">Branches: CPCanceling → CPEndsAtPeriodEnd · CPPaymentProblem → retry · CPExpired → new purchase</text>
|
||||
<text x="25" y="203" class="h">BOT PAYMENT</text><rect class="box pay" rx="8" x="165" y="155" width="1000" height="85"/><text x="185" y="181" class="b">BPPrepared → BPCheckoutOpen / BPAwaitingPayment → BPVerifying → BPPaidOneTime / BPSubscriptionActive</text><text x="185" y="205" class="b">Provider branches: BPGrace · BPOnHold · BPPaused · BPEndsAtPeriodEnd · BPExpired · BPRefunded · BPRevoked</text><text x="185" y="226" class="s">Provider messages and webhooks change this lane only.</text>
|
||||
<text x="25" y="303" class="h">SERVICE GRANT</text><rect class="box grant" rx="8" x="165" y="265" width="1000" height="65"/><text x="185" y="292" class="b">GrantReady → GrantFulfilled</text><text x="520" y="292" class="b">or GrantReady → GrantRevoked</text><text x="185" y="315" class="s">The only payment-to-service interface; unique by payment + product + monthly grant period.</text>
|
||||
<text x="25" y="403" class="h">BOT BADGE</text><rect class="box badge" rx="8" x="165" y="355" width="1000" height="85"/><text x="185" y="382" class="b">BBRequested → BBSigning → BBIssued</text><text x="520" y="382" class="b">or BBRetryableFailure / BBFinalFailure</text><text x="185" y="407" class="s">Marks grant fulfilled only when the credential is durably cached.</text><text x="185" y="427" class="s">It has no installed state and does not read provider/payment state.</text>
|
||||
<text x="25" y="513" class="h">CLIENT BADGE</text><rect class="box client" rx="8" x="165" y="465" width="1000" height="85"/><text x="185" y="492" class="b">CBNone → CBNeeded → CBRequesting → CBReceived → CBInstalling → CBInstalled</text><text x="185" y="517" class="b">Branches: CBRetryableFailure → retry · CBFinalFailure → update/support</text><text x="185" y="538" class="s">An installed active badge remains visible while any payment operation fails.</text>
|
||||
<path class="dash" d="M665 130 V155"/><path class="line" d="M665 240 V265"/><path class="line" d="M665 330 V355"/><path class="dash" d="M665 440 V465"/><text x="600" y="595" text-anchor="middle" class="muted b">Arrows between lanes are typed messages or PaymentCredit—not shared mutable state.</text></svg>
|
||||
<path class="dash" d="M665 130 V155"/><path class="line" d="M665 240 V265"/><path class="line" d="M665 330 V355"/><path class="dash" d="M665 440 V465"/><text x="600" y="595" text-anchor="middle" class="muted b">Arrows between lanes are typed messages or ServiceGrant—not shared mutable state.</text></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Reference in New Issue
Block a user