Compare commits
2 Commits
031b8cc062
...
3bad1451a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bad1451a9 | ||
|
|
0f1a309be3 |
@ -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">
|
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug b-4">
|
||||||
<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,9 +518,15 @@ function PlanBusiness({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={current}
|
disabled={current}
|
||||||
onClick={onUpgrade}
|
onClick={onUpgrade}
|
||||||
className="mt-auto pt-6 w-full"
|
className="mt-3 p-6 w-full"
|
||||||
>
|
>
|
||||||
{current ? "Plan actuel" : "Passer Business"}
|
{current ? (
|
||||||
|
"Plan actuel"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Passer Business <ArrowRight size={14} aria-hidden="true" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
60
bruno/08-Checkin/03 In-app pending.bru
Normal file
60
bruno/08-Checkin/03 In-app pending.bru
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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.
|
||||||
|
}
|
||||||
44
bruno/08-Checkin/04 In-app respond paid.bru
Normal file
44
bruno/08-Checkin/04 In-app respond paid.bru
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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.
|
||||||
|
}
|
||||||
44
bruno/08-Checkin/05 In-app respond pending.bru
Normal file
44
bruno/08-Checkin/05 In-app respond pending.bru
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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).
|
||||||
|
}
|
||||||
65
bruno/09-Billing/01 Get subscription.bru
Normal file
65
bruno/09-Billing/01 Get subscription.bru
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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.
|
||||||
|
}
|
||||||
63
bruno/09-Billing/02 Start checkout.bru
Normal file
63
bruno/09-Billing/02 Start checkout.bru
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
50
bruno/09-Billing/03 Open portal.bru
Normal file
50
bruno/09-Billing/03 Open portal.bru
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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`.
|
||||||
|
}
|
||||||
55
bruno/09-Billing/04 Reactivate.bru
Normal file
55
bruno/09-Billing/04 Reactivate.bru
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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`
|
||||||
|
}
|
||||||
66
bruno/09-Billing/folder.bru
Normal file
66
bruno/09-Billing/folder.bru
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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,6 +48,12 @@ 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