Compare commits
No commits in common. "3bad1451a96b39fcd965963065c7c32b9dc5276e" and "031b8cc0621630bd838eba9bc210287819c052f0" have entirely different histories.
3bad1451a9
...
031b8cc062
@ -504,7 +504,7 @@ function PlanBusiness({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-[11.5px] text-ink-3 mb-5">{totalLabel}</p>
|
<p className="text-[11.5px] text-ink-3 mb-5">{totalLabel}</p>
|
||||||
|
|
||||||
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug b-4">
|
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
|
||||||
<FeatureLine strong>Tout Pro, plus :</FeatureLine>
|
<FeatureLine strong>Tout Pro, plus :</FeatureLine>
|
||||||
<FeatureLine>5 sièges utilisateurs</FeatureLine>
|
<FeatureLine>5 sièges utilisateurs</FeatureLine>
|
||||||
<FeatureLine>Réponses depuis votre email pro</FeatureLine>
|
<FeatureLine>Réponses depuis votre email pro</FeatureLine>
|
||||||
@ -518,15 +518,9 @@ function PlanBusiness({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={current}
|
disabled={current}
|
||||||
onClick={onUpgrade}
|
onClick={onUpgrade}
|
||||||
className="mt-3 p-6 w-full"
|
className="mt-auto pt-6 w-full"
|
||||||
>
|
>
|
||||||
{current ? (
|
{current ? "Plan actuel" : "Passer Business"}
|
||||||
"Plan actuel"
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Passer Business <ArrowRight size={14} aria-hidden="true" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 03 In-app pending
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
url: {{baseUrl}}/api/v1/checkin/inapp/pending
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("data est un array", function () {
|
|
||||||
expect(res.getBody().data).to.be.an("array");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
GET /api/v1/checkin/inapp/pending — auth requise
|
|
||||||
|
|
||||||
Liste les factures de l'org en `awaiting_user_confirmation` (= ayant
|
|
||||||
reçu l'email check-in mais pas encore eu de réponse). C'est la queue
|
|
||||||
que la modale du SPA affiche au login pour rappeler à l'user qu'il a
|
|
||||||
des décisions à prendre.
|
|
||||||
|
|
||||||
Tri : `due_date` ASC (les plus anciennes échéances d'abord).
|
|
||||||
|
|
||||||
Réponse :
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "uuid",
|
|
||||||
"numero": "F2026-0007",
|
|
||||||
"amountTtcCents": 12345,
|
|
||||||
"issueDate": "2026-04-07T...",
|
|
||||||
"dueDate": "2026-05-07T...",
|
|
||||||
"status": "awaiting_user_confirmation",
|
|
||||||
"clientName": "Boulangerie Martin",
|
|
||||||
"planName": "Standard B2B"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Note V1
|
|
||||||
|
|
||||||
En prod actuelle, le statut DB de l'invoice reste `pending` jusqu'à ce
|
|
||||||
que l'user réponde au check-in (le `send_checkin_job` ne change pas
|
|
||||||
le statut). C'est le seed démo qui force `awaiting_user_confirmation`
|
|
||||||
pour pré-peupler des cas. À aligner V1.5.
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 04 In-app respond paid
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{baseUrl}}/api/v1/checkin/inapp/{{invoiceId}}/paid
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("status = paid", function () {
|
|
||||||
expect(res.getBody().data.status).to.equal("paid");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
POST /api/v1/checkin/inapp/:invoiceId/paid — auth requise
|
|
||||||
|
|
||||||
Réponse "Oui — la facture est payée" depuis l'app (modale check-in OU
|
|
||||||
bouton "Marquer encaissée" sur la fiche). Effets identiques au lien
|
|
||||||
email mais auth-based (pas de token URL).
|
|
||||||
|
|
||||||
Effets :
|
|
||||||
- CheckinTask (si elle existe) : status='answered', answer='paid'
|
|
||||||
- Invoice : status='paid', paid_at=now, rubis_earned+1
|
|
||||||
- Organization.rubis_count+1
|
|
||||||
- ActivityEvent kind=invoice_paid (label "via confirmation")
|
|
||||||
- Toutes les RelanceTask scheduled de la facture → cancelled
|
|
||||||
|
|
||||||
Réponse : la facture sérialisée (status='paid' désormais).
|
|
||||||
|
|
||||||
Précondition : `invoiceId` doit appartenir à l'org de l'user (404 sinon).
|
|
||||||
Idempotent : si déjà paid → renvoie l'état courant sans bumper.
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 05 In-app respond pending
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{baseUrl}}/api/v1/checkin/inapp/{{invoiceId}}/pending
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("status = in_relance", function () {
|
|
||||||
expect(res.getBody().data.status).to.equal("in_relance");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
POST /api/v1/checkin/inapp/:invoiceId/pending — auth requise
|
|
||||||
|
|
||||||
Réponse "Non — toujours en attente, lance les relances" depuis l'app
|
|
||||||
(modale check-in OU bouton "Relancer maintenant" sur la fiche).
|
|
||||||
|
|
||||||
Effets :
|
|
||||||
- CheckinTask (si elle existe) : status='answered', answer='still_pending'
|
|
||||||
- `scheduleRelancesForInvoice()` : crée les RelanceTask scheduled selon
|
|
||||||
le plan, enqueue dans BullMQ. Si certaines étapes sont déjà passées
|
|
||||||
(catch-up), la 1re part à `now+1min`.
|
|
||||||
- Invoice.status passe immédiatement en `in_relance` (l'user voit la
|
|
||||||
facture sortir de l'attente sans attendre le 1er envoi mail)
|
|
||||||
- ActivityEvent kind=relance_sent (label "Relances activées pour …")
|
|
||||||
|
|
||||||
Réponse : la facture sérialisée (status='in_relance' désormais).
|
|
||||||
|
|
||||||
Précondition : `invoiceId` doit appartenir à l'org (404 sinon).
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 01 Get subscription
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
url: {{baseUrl}}/api/v1/billing/subscription
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("plan dans l'enum", function () {
|
|
||||||
expect(res.getBody().data.plan).to.be.oneOf(["free", "pro", "business"]);
|
|
||||||
});
|
|
||||||
test("caps présents", function () {
|
|
||||||
expect(res.getBody().data.caps).to.have.property("activeInvoicesLimit");
|
|
||||||
expect(res.getBody().data.caps).to.have.property("seatsLimit");
|
|
||||||
});
|
|
||||||
test("flags grace + cancel exposés", function () {
|
|
||||||
expect(res.getBody().data).to.have.property("inGracePeriod");
|
|
||||||
expect(res.getBody().data).to.have.property("cancelAtPeriodEnd");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
GET /api/v1/billing/subscription — auth requise
|
|
||||||
|
|
||||||
Retourne l'état de la souscription pour l'org de l'user courant.
|
|
||||||
|
|
||||||
Réponse :
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"plan": "free" | "pro" | "business",
|
|
||||||
"caps": {
|
|
||||||
"activeInvoicesLimit": 5 | null,
|
|
||||||
"seatsLimit": 1 | 5 | null,
|
|
||||||
"multiUsers": false | true,
|
|
||||||
"replyFromUserEmail": false | true,
|
|
||||||
"smsEnabled": false
|
|
||||||
},
|
|
||||||
"activeInvoicesCount": 12,
|
|
||||||
"inGracePeriod": true | false,
|
|
||||||
"gracePeriodEndsAt": "2026-08-06T..." | null,
|
|
||||||
"subscriptionStatus": "active" | "past_due" | "canceled" | null,
|
|
||||||
"billingCycle": "monthly" | "yearly" | null,
|
|
||||||
"currentPeriodEnd": "2026-06-07T..." | null,
|
|
||||||
"hasStripeCustomer": true | false,
|
|
||||||
"cancelAtPeriodEnd": false | true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Utilisé par `/parametres/abonnement` côté SPA pour afficher le plan
|
|
||||||
courant + les caps + l'état d'annulation.
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 02 Start checkout
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{baseUrl}}/api/v1/billing/checkout
|
|
||||||
body: json
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
body:json {
|
|
||||||
{
|
|
||||||
"plan": "pro",
|
|
||||||
"cycle": "monthly"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("URL Stripe Checkout retournée", function () {
|
|
||||||
expect(res.getBody().data.url).to.match(/^https:\/\/checkout\.stripe\.com\//);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
POST /api/v1/billing/checkout — auth requise
|
|
||||||
|
|
||||||
Crée une Stripe Checkout Session pour upgrader vers Pro ou Business.
|
|
||||||
Body :
|
|
||||||
```json
|
|
||||||
{ "plan": "pro" | "business", "cycle": "monthly" | "yearly" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Effets :
|
|
||||||
- Crée le Stripe Customer si l'org n'en a pas (idempotent via `stripeCustomerId`)
|
|
||||||
- Crée une Checkout Session en mode `subscription` avec le bon Price (lookup_key)
|
|
||||||
- `subscription_data.metadata.organization_id` posé pour le webhook
|
|
||||||
- `allow_promotion_codes: true`, `locale: "fr"`, billing address auto
|
|
||||||
|
|
||||||
Réponse :
|
|
||||||
```json
|
|
||||||
{ "data": { "url": "https://checkout.stripe.com/c/pay/cs_test_..." } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Le SPA fait `window.location.href = url`. L'user paye sur Stripe (UI hostée),
|
|
||||||
puis Stripe le redirect vers `${WEB_URL}/parametres/abonnement?checkout=success`.
|
|
||||||
|
|
||||||
## Pour tester
|
|
||||||
|
|
||||||
1. Récupère un `token` via Auth → 01 Signup
|
|
||||||
2. Lance cette requête → tu reçois une URL Checkout
|
|
||||||
3. Ouvre l'URL dans un navigateur, paye avec CB test : `4242 4242 4242 4242`
|
|
||||||
4. Vérifie via **01 Get subscription** que `plan: "pro"` est posé
|
|
||||||
5. Mailpit (http://localhost:8025) : tu reçois un email "thank you for subscribing"
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 03 Open portal
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{baseUrl}}/api/v1/billing/portal
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("URL Stripe Billing Portal retournée", function () {
|
|
||||||
expect(res.getBody().data.url).to.match(/^https:\/\/billing\.stripe\.com\//);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
POST /api/v1/billing/portal — auth requise
|
|
||||||
|
|
||||||
Crée une Stripe Billing Portal Session pour que l'user gère lui-même
|
|
||||||
sa souscription (changement de CB, factures Stripe, annulation).
|
|
||||||
|
|
||||||
Précondition : l'org doit avoir un `stripeCustomerId` (= avoir déjà
|
|
||||||
passé un checkout au moins une fois). Sinon → 400 `no_stripe_customer`.
|
|
||||||
|
|
||||||
Réponse :
|
|
||||||
```json
|
|
||||||
{ "data": { "url": "https://billing.stripe.com/p/session/..." } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Le SPA fait `window.location.href = url`. L'user fait ce qu'il veut
|
|
||||||
côté Stripe Portal, puis le redirect retourne vers
|
|
||||||
`${WEB_URL}/parametres/abonnement`.
|
|
||||||
|
|
||||||
## Annulation via portail
|
|
||||||
|
|
||||||
Quand l'user clique "Cancel plan" + confirme, Stripe pose `cancel_at`
|
|
||||||
(timestamp = period_end) et fire `customer.subscription.updated`. Notre
|
|
||||||
webhook détecte les 2 mécaniques (`cancel_at_period_end` ET `cancel_at`)
|
|
||||||
et les unifie en `org.cancel_at_period_end = true`.
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: 04 Reactivate
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
url: {{baseUrl}}/api/v1/billing/reactivate
|
|
||||||
body: none
|
|
||||||
auth: bearer
|
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{token}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests {
|
|
||||||
test("200 OK", function () {
|
|
||||||
expect(res.getStatus()).to.equal(200);
|
|
||||||
});
|
|
||||||
test("ok=true", function () {
|
|
||||||
expect(res.getBody().data.ok).to.equal(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
POST /api/v1/billing/reactivate — auth requise
|
|
||||||
|
|
||||||
Annule une annulation programmée — la subscription continue son cycle
|
|
||||||
normal, sans paiement immédiat.
|
|
||||||
|
|
||||||
Effets :
|
|
||||||
- Retrieve le sub Stripe pour savoir laquelle des 2 mécaniques est posée
|
|
||||||
(Stripe REFUSE qu'on passe `cancel_at_period_end` ET `cancel_at` dans
|
|
||||||
le même update : "Please pass in only one")
|
|
||||||
- Si `cancel_at` est posé → on envoie `cancel_at: null`
|
|
||||||
- Sinon → on envoie `cancel_at_period_end: false`
|
|
||||||
- Persiste sur l'org : `cancel_at_period_end = false`
|
|
||||||
|
|
||||||
Idempotent :
|
|
||||||
- Si l'org n'a pas de sub Stripe → 400 `no_active_subscription`
|
|
||||||
- Si l'annulation n'est PAS programmée → 200 `{ok: true}` sans toucher Stripe
|
|
||||||
|
|
||||||
Réponse :
|
|
||||||
```json
|
|
||||||
{ "data": { "ok": true } }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pour tester
|
|
||||||
|
|
||||||
1. Annule la sub via le Customer Portal (03 Open portal → click Cancel)
|
|
||||||
2. Vérifie via **01 Get subscription** : `cancelAtPeriodEnd: true`
|
|
||||||
3. Lance cette requête
|
|
||||||
4. Re-vérifie : `cancelAtPeriodEnd: false`
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
meta {
|
|
||||||
name: Billing
|
|
||||||
seq: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
docs {
|
|
||||||
## Billing — Stripe Checkout, Customer Portal, Subscriptions
|
|
||||||
|
|
||||||
Endpoints qui pilotent la souscription d'une org : passage Free → Pro/Business
|
|
||||||
via Stripe Checkout, gestion CB & annulation via Customer Portal, et webhook
|
|
||||||
Stripe pour tenir l'org synchro avec l'état Stripe.
|
|
||||||
|
|
||||||
## Plans
|
|
||||||
|
|
||||||
| Plan | Mensuel | Annuel | Limite factures actives | V2 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Free | 0 € | — | 5 (post-grace 3 mois) | — |
|
|
||||||
| Pro | 19 € | 190 € | illimité | — |
|
|
||||||
| Business | 49 € | 490 € | illimité | 5 sièges, reply-from-user |
|
|
||||||
|
|
||||||
## Flow d'upgrade
|
|
||||||
|
|
||||||
1. SPA → **02 Start checkout** (auth) avec `{plan, cycle}` → reçoit l'URL Stripe
|
|
||||||
2. SPA redirect vers l'URL → user paye sur Stripe Checkout (UI hostée)
|
|
||||||
3. Stripe redirect vers `?checkout=success` côté SPA
|
|
||||||
4. **En parallèle** : Stripe envoie `checkout.session.completed` au webhook
|
|
||||||
→ `applySubscriptionToOrg` set `org.plan='pro'`, `subscription_status='active'`
|
|
||||||
5. SPA repoll **01 Get subscription** → affiche le nouveau plan
|
|
||||||
|
|
||||||
## Flow d'annulation
|
|
||||||
|
|
||||||
1. SPA → **03 Open portal** (auth) → reçoit l'URL portal
|
|
||||||
2. SPA redirect → user click "Cancel plan" + confirme
|
|
||||||
3. Stripe pose `cancel_at` (timestamp = period_end) ou `cancel_at_period_end=true`
|
|
||||||
4. Stripe envoie `customer.subscription.updated` au webhook
|
|
||||||
→ `org.cancel_at_period_end = true`
|
|
||||||
5. UI affiche bandeau "ANNULÉ — accès jusqu'au DD/MM" + bouton "Réactiver"
|
|
||||||
6. À `current_period_end`, Stripe envoie `customer.subscription.deleted`
|
|
||||||
→ `org.plan = 'free'`
|
|
||||||
|
|
||||||
## Flow de réactivation
|
|
||||||
|
|
||||||
Si l'user clique "Réactiver" avant la fin de période :
|
|
||||||
1. SPA → **04 Reactivate** (auth)
|
|
||||||
2. Backend retrieve la sub Stripe pour savoir si `cancel_at` ou
|
|
||||||
`cancel_at_period_end` est posé (ils sont mutuellement exclusifs côté
|
|
||||||
Stripe API), puis clear le bon
|
|
||||||
3. Pas de paiement immédiat — la sub continue son cycle normal
|
|
||||||
|
|
||||||
## Webhook idempotence
|
|
||||||
|
|
||||||
Stripe peut re-livrer un event plusieurs fois. Notre handler est read-then-write
|
|
||||||
(pas d'assumption "1-shot"). Les 4 events pris en charge :
|
|
||||||
- checkout.session.completed
|
|
||||||
- customer.subscription.created/updated → applySubscriptionToOrg
|
|
||||||
- customer.subscription.deleted → bascule en Free
|
|
||||||
- invoice.payment_failed → status past_due
|
|
||||||
|
|
||||||
## Pour tester en local
|
|
||||||
|
|
||||||
1. `node ace stripe:setup` une fois pour créer Products + Prices test mode
|
|
||||||
2. `stripe listen --forward-to localhost:3333/api/v1/billing/webhook`
|
|
||||||
→ copie le `whsec_...` dans `.env` (STRIPE_WEBHOOK_SECRET)
|
|
||||||
3. Lance Bruno **02 Start checkout** → ouvre l'URL → CB test 4242 4242 4242 4242
|
|
||||||
4. Reviens sur **01 Get subscription** → tu devrais voir `plan: "pro"`
|
|
||||||
}
|
|
||||||
@ -48,12 +48,6 @@ Définies dans `environments/local.bru`. Les valeurs **vides** (token, userId, e
|
|||||||
11. **Imports → 01 Upload (mock)** (capture `batchId` + `draftId`)
|
11. **Imports → 01 Upload (mock)** (capture `batchId` + `draftId`)
|
||||||
12. **Imports → 02 Get batch** (review des drafts pending)
|
12. **Imports → 02 Get batch** (review des drafts pending)
|
||||||
13. **Imports → 03 Validate draft** (transforme le draft en facture)
|
13. **Imports → 03 Validate draft** (transforme le draft en facture)
|
||||||
14. **Checkin → 03 In-app pending** (liste les factures `awaiting_user_confirmation`)
|
|
||||||
15. **Checkin → 04 In-app respond paid** ou **05 In-app respond pending**
|
|
||||||
16. **Billing → 01 Get subscription** (state du plan + caps)
|
|
||||||
17. **Billing → 02 Start checkout** (upgrade Pro/Business via Stripe Checkout)
|
|
||||||
18. **Billing → 03 Open portal** (gérer CB / annuler via Stripe Portal)
|
|
||||||
19. **Billing → 04 Reactivate** (annule l'annulation programmée)
|
|
||||||
|
|
||||||
### Flow refresh (silent re-login)
|
### Flow refresh (silent re-login)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user