# Abonnementen technisch integreren in uw toepassing

Verbind Bookelio op de server, beheer abonnementen en rechten via de API, stel prorata in en ken gratis maanden toe.

Taal : nl
Bijgewerkt op : 2026-09-09
Bron : https://app.staging.bookelio.app/docs/nl/abonnementen-api-integratie

## De verantwoordelijkheden begrijpen

Uw toepassing authenticeert haar gebruikers en identificeert hun account of organisatie. Bookelio bewaart de commerciële catalogus, de te factureren klanten, de abonnementen en hun betaalperiodes. Uw server vraagt Bookelio om de verkochte functies toe te staan. Een knop in de browser verbergen vervangt deze servercontrole niet.

De verkopende organisatie bezit een of meer softwaretoepassingen. Elke softwaretoepassing heeft haar eigen functies, plannen en abonnementen. De module kan dus meerdere toepassingen en meerdere verkopers bedienen, elk binnen hun eigen omgeving.

De onderstaande voorbeelden zijn fictief. Vervang de domeinen en identificatiegegevens door die van uw installatie. Raadpleeg [Softwareabonnementen verkopen](/docs/nl/softwareabonnementen) voor de werkwijze in de schermen.

## De verbinding en rechten voorbereiden

Activeer de module **Softwareabonnementen** voor de verkopende organisatie. Bereid een facturatieklant in die organisatie voor en een btw-beleid voor betalende aanbiedingen. Configureer haar betaalmethoden en e-maildienst voor betalingen en meldingen.

Maak een API-sleutel van de verkopende organisatie aan met de [geschikte rechten](/docs/nl/team-en-toegangsrechten). Bewaar die op de server van uw toepassing. De sleutel identificeert de verkoper; hij vervangt de authenticatie van uw gebruikers niet en kan toegang geven tot abonnementen van meerdere klanten van dezelfde verkoper.

| Gebruik | Rechten van de sleutel |
| --- | --- |
| Software, plannen, abonnementen en toegestane functies lezen | `softwareBilling.read` |
| De catalogus beheren, opzeggen, aanbiedingen toekennen of gebruikersplaatsen plannen | `softwareBilling.manage` |
| Functies en externe identiteiten registreren | `softwareBilling.manage` en `customers.create` |
| Een abonnement aanmaken | `softwareBilling.manage` en `paymentRequests.create` |
| Een abonnement wijzigen met een `planId`, onder meer om het opnieuw te activeren | `softwareBilling.manage` en `paymentRequests.create` |
| Elke `IMMEDIATE`-wijziging toepassen, ook zonder toeslag of voor alleen gebruikersplaatsen | `softwareBilling.manage` en `paymentRequests.create` |
| Het facturatiemoment in de verkopersinstellingen lezen of wijzigen | `settings.read` om te lezen; `settings.update` om te wijzigen |

Om klanten en btw-beleidsregels via hun eigen API's aan te maken of te lezen, hebt u ook de rechten voor die resources nodig. De voorbeelden gaan ervan uit dat die elementen al zijn aangemaakt.

De REST-routes hebben het voorvoegsel `/api/v1/admin/software-billing`. Het oRPC-transport gebruikt `/rpc/v1` en de bewerkingen `admin.softwareBilling.*`. De specificatie op de API-instantie is te raadplegen via `/spec/v1.json`. In de Bookelio-monorepo beschrijft het type `BookelioApiV1Client` dit contract; een externe toepassing kan rechtstreeks REST gebruiken, zonder afhankelijk te zijn van een intern package uit de repository.

## HTTP-aanroepen op de server voorbereiden

In de voorbeelden zijn de onderstaande variabelen namen die de externe toepassing zelf kiest. Ze configureren Bookelio zelf niet automatisch.

```dotenv
SOFTWARE_BILLING_API_URL=https://api.billing.example.com
SOFTWARE_BILLING_API_KEY=REPLACE_WITH_SERVER_SIDE_KEY
SOFTWARE_BILLING_APPLICATION_ID=REPLACE_WITH_APPLICATION_ID
```

Hieronder staat een JavaScript-hulpfunctie die uitsluitend op de server gebruikt mag worden, in een omgeving met `fetch`. Ze roept de REST-routes aan met een eenvoudige JSON-body. Ontvangen datums zijn ISO 8601-tekenreeksen; de oRPC-client beheert zelf de serialisatie van `Date`-waarden.

```js
const billingBase = new URL(
  '/api/v1/admin/software-billing/',
  process.env.SOFTWARE_BILLING_API_URL,
);

async function billingRequest(path, { method = 'GET', body } = {}) {
  const response = await fetch(new URL(path, billingBase), {
    method,
    headers: {
      'x-api-key': process.env.SOFTWARE_BILLING_API_KEY,
      'content-type': 'application/json',
    },
    body: body === undefined ? undefined : JSON.stringify(body),
    redirect: 'error',
    signal: AbortSignal.timeout(10_000),
  });
  if (!response.ok) {
    throw new Error(`Bookelio billing request failed: HTTP ${response.status}`);
  }
  return response.json();
}
```

Plaats de sleutel niet in een URL, browsercomponent of publieke variabele. De verbindings-URL is die van de API; betaalpagina's worden gehost door het dashboard van de verkoper, dat een ander domein kan hebben.

## De software en plannen aanmaken

Het instellen van de catalogus is een beheerhandeling die u eenmalig uitvoert, niet telkens wanneer een gebruiker zich aanmeldt. Bewaar de `id`-waarden die Bookelio terugstuurt. De `code`-waarden zijn leesbare referenties en vervangen deze identificatiegegevens niet in aanroepen.

Om een catalogus te hergebruiken die al in het dashboard is aangemaakt, geeft `GET /applications` het resultaat `{ applications: [...] }` terug en `GET /applications/{applicationId}/plans` het resultaat `{ plans: [...] }`, onder het eerder vermelde REST-voorvoegsel. Die laatste lijst bevat ook inactieve plannen: filter op `isActive` voordat u een aanbieding te koop aanbiedt. Met `GET /applications/{applicationId}/subscriptions` kan de verkoper abonnementen opvragen, met als antwoord `{ subscriptions: [...] }`.

```js
const application = await billingRequest('applications', {
  method: 'POST',
  body: {
    name: 'Example Workspace',
    code: 'example-workspace',
    features: [
      { key: 'projects', name: 'Projecten' },
      { key: 'exports', name: 'Exports' },
    ],
  },
});

const enterprise = await billingRequest('plans', {
  method: 'POST',
  body: {
    applicationId: application.id,
    name: 'Enterprise',
    code: 'enterprise-monthly',
    interval: 'MONTHLY',
    basePriceCents: 400000,
    seatPriceCents: 0,
    includedSeats: 0,
    trialDays: 14,
    gracePeriodDays: 7,
    vatPolicyId: 'REPLACE_WITH_SELLER_VAT_POLICY_ID',
    featureKeys: ['projects', 'exports'],
  },
});
```

Dit plan kost **€ 4.000 exclusief btw per maand**, ongeacht het aantal gebruikersplaatsen, na een proefperiode van 14 dagen. Daarna biedt het zeven dagen respijt aan het begin van elke betalende periode om tijd voor betaling te geven. `seatPriceCents: 0` schakelt de toeslag per gebruikersplaats uit; de API vereist geen veld `pricingMode`.

| Fictief model | `basePriceCents` | `seatPriceCents` | `includedSeats` | Resultaat exclusief btw |
| --- | --- | --- | --- | --- |
| Gratis | `0` | `0` | `0` | Geen betaling |
| Vast Enterprise-plan | `400000` | `0` | `0` | € 4.000 per periode, ongeacht het aantal gebruikers |
| Basis en extra gebruikersplaatsen | `1000` | `300` | `2` | € 19 per periode voor 5 gebruikersplaatsen |
| Alle gebruikersplaatsen betalend | `0` | `500` | `0` | € 25 per periode voor 5 gebruikersplaatsen |

De prijzen zijn gehele getallen in eurocenten exclusief btw. `interval` heeft de waarde `MONTHLY` of `YEARLY`: geef bij een jaarlijks aanbod de prijs voor het volledige jaar op, geen maandprijs die nog vermenigvuldigd moet worden. `trialDays` loopt van 0 tot 365; nul schakelt de proefperiode uit. Een volledig gratis plan maakt geen proefperiode voor een betalend abonnement en geen betaalverzoek aan, ook niet wanneer een duur is ingevuld.

`gracePeriodDays` is een geheel getal van 0 tot 365, onafhankelijk van de proefperiode. Het is optioneel bij aanmaak: weglaten betekent `0` en behoudt de werking zonder respijtperiode. Bij `PATCH /plans/{planId}` behoudt weglaten de huidige waarde; `0` versturen schakelt respijt voor toekomstige periodes uit. Bestaande plannen blijven zonder respijtperiode totdat deze waarde wordt gewijzigd.

De sleutels in `featureKeys` moeten al bestaan in de softwaretoepassing. Een btw-beleid van de verkoper is verplicht zodra een vaste prijs of prijs per gebruikersplaats positief is. Het aantal gebruikersplaatsen van een abonnement is een geheel getal van 1 tot 100.000. Deze technische grens maakt van het vaste tarief geen prijs per gebruikersplaats en is geen commerciële gebruikerslimiet die in het plan is opgenomen.

## De klant identificeren en zijn abonnement aanmaken

`customerId` verwijst naar de te factureren klant in de verkopende organisatie. `externalId` verwijst naar het account van die klant in uw toepassing, bijvoorbeeld een vaste organisatie-ID. Bewaar deze koppeling op de server. Vermijd een naam of e-mailadres dat kan wijzigen.

De identiteit is uniek per combinatie `(applicationId, externalId)`. In de volgende code moet `accountId` afkomstig zijn uit uw geverifieerde sessie en uw controles op lidmaatschap, niet uit een vrij te kiezen identificatiegegeven dat de browser verstuurt. Ook het plan en de klant moeten op de server worden geselecteerd en gevalideerd.

```js
const subscription = await billingRequest('subscriptions', {
  method: 'POST',
  body: {
    applicationId: process.env.SOFTWARE_BILLING_APPLICATION_ID,
    planId: selectedPlanId,
    customerId: billingCustomerId,
    externalId: accountId,
    seats: memberCount,
  },
});
// Bewaar subscription.id voor wijzigingen en opzeggingen.
```

Een nieuwe poging met dezelfde software, externe referentie, klant en hetzelfde plan vindt het bestaande abonnement terug. Ze herstart de proefperiode niet, factureert niet opnieuw en wijzigt het aantal gebruikersplaatsen niet. Een andere klant of een ander plan voor die referentie veroorzaakt een conflict. Gebruik de wijzigingsbewerking om van plan te veranderen; controleer bij een andere klant uw identiteitskoppeling, want deze bewerking vervangt de facturatieklant niet. Software en plannen aanmaken werkt niet als aanmaken-of-bijwerken; herhaal die bewerkingen niet zoals bij het inschrijven van een klant.

## Functies controleren vóór een actie

Gebruik `subscriptions.entitlements`, niet alleen de opgeslagen status of de naam van het plan. Deze opvraag controleert de huidige periode, de nettobetaling ervoor en de eventuele respijtperiode, en geeft de functiesleutels terug die voor die periode zijn vastgelegd.

```js
async function canUseFeature(verifiedAccountId, featureKey) {
  const appId = encodeURIComponent(
    process.env.SOFTWARE_BILLING_APPLICATION_ID,
  );
  const externalId = encodeURIComponent(verifiedAccountId);
  try {
    const rights = await billingRequest(
      `applications/${appId}/entitlements/${externalId}`,
    );
    return rights.access === true
      && typeof rights.validUntil === 'string'
      && Date.parse(rights.validUntil) > Date.now()
      && Array.isArray(rights.featureKeys)
      && rights.featureKeys.includes(featureKey);
  } catch {
    // Weiger tijdelijk toegang; stel betalende functies niet open.
    return false;
  }
}
```

Fictief voorbeeld van een REST-antwoord:

```json
{
  "subscriptionId": "example-subscription",
  "status": "PAST_DUE",
  "access": true,
  "featureKeys": ["projects", "exports"],
  "validUntil": "2026-09-29T10:00:00.000Z",
  "seats": 5,
  "isInGracePeriod": true,
  "graceEndsAt": "2026-09-29T10:00:00.000Z"
}
```

Dit voorbeeld toont de eerste betalende periode na een proefperiode die op 22 september om 10.00 uur UTC eindigde: er wordt nog op betaling gewacht, maar de rechten blijven open tot 29 september om 10.00 uur UTC dankzij de zeven ingestelde dagen. Alleen `PAST_DUE` volstaat dus niet om een actie te weigeren; gebruik `access`, de gevraagde sleutel en `validUntil`, zoals in de hulpfunctie.

Zonder abonnement bevat het antwoord `subscriptionId: null`, `status: null`, `access: false`, `featureKeys: []`, `validUntil: null`, `seats: 0`, `isInGracePeriod: false` en `graceEndsAt: null`. Als u een cache toevoegt, scheid die dan per verkoper, softwaretoepassing en externe referentie, beperk de bewaartijd en verleen nooit toegang na `validUntil`. De module biedt geen uitgaande webhook voor abonnementswijzigingen naar de toepassing: voorzie deze opvragen op de controlepunten van uw server.

De leesbewerkingen `entitlements` en `getByExternalId`, en ook `syncSeats`, behouden hun controles op sleutel, rechten en verkoper, maar blijven toegankelijk zonder de toegang van de verkoper tot de module-interface te controleren. Zo kunnen bestaande abonnementen worden geraadpleegd en in orde gebracht; de overige bewerkingen blijven beschermd door de activering van de module.

## De respijtperiode voor betaling toepassen

Bij aanmaak bewaart de periode een momentopname van `gracePeriodDays`; `graceEndsAt` wordt berekend uit die waarde en de onveranderlijke datums van de periode. Voor een betalende periode loopt het respijt vanaf `periodStart` tot de vroegste van twee datums: `periodStart + gracePeriodDays × 24 uur` of `periodEnd`. Het eindmoment is niet inbegrepen: op dat tijdstip verleent een nog onbetaalde periode geen toegang meer. Het respijt geldt ook voor de eerste betalende periode en die na de proefperiode, zonder facturatiedatums te wijzigen of een nieuwe proefperiode aan te maken.

Tijdens het respijt blijft de berekende status `PAST_DUE`, is `access` `true`, is `isInGracePeriod` `true` en is `validUntil` gelijk aan `graceEndsAt`. Een geverifieerde volledige betaling zet de rechten om naar een betaalde periode: `isInGracePeriod: false` en `validUntil: periodEnd`. Verlopen respijt met nog onvoldoende betaling geeft `access: false`, `featureKeys: []` en `validUntil: null`, zonder op de worker te wachten.

`graceEndsAt` is metadata, geen bewijs van toegang: de datum blijft zichtbaar na betaling of afloop. Ze is `null` voor een periode zonder respijt, een gratis periode of een proefperiode. Alleen `isInGracePeriod` geeft aan dat de huidige toegang dit uitstel gebruikt. Voeg geen lokaal respijt toe als de API faalt en verleng geen cache tot die datum zonder `access` en `validUntil` te controleren.

Deze velden zijn toegevoegd in compatibele revisie V1-r28. Antwoorden van een oudere V1-instantie kunnen `gracePeriodDays`, `graceEndsAt` en `isInGracePeriod` weglaten: leid uit hun afwezigheid geen respijt af en blijf `access` en `validUntil` gebruiken om te beslissen. De verkopende instantie moet worden bijgewerkt om deze functie in te stellen en toe te passen.

De duur en datum liggen voor elke periode vast: een planwijziging, nieuwe poging van de worker of late betaling verschuift ze niet. De worker moet eerst de nieuwe periode aanmaken; vóór die uitvoering verleent een oude verlopen periode geen voorafgaand respijt. Een verwerkingsvertraging kan dus een korte onderbreking veroorzaken, maar kan het begin van de respijtperiode niet uitstellen tot het moment waarop de worker draait.

Een gedeeltelijke betaling verlengt de respijtperiode niet. Een voltooide of lopende terugbetaling verhindert respijt; een mislukte terugbetaling (`FAILED`) blokkeert het niet op zichzelf. Een effectieve opzegging weigert alle toegang en een geplande opzegging verlengt rechten niet voorbij hun grens. De respijtperiode markeert geen betaling als voltooid, start op zichzelf geen factuur en staat geen nieuwe betaalverzoeken toe zolang de periode onbetaald blijft.

## De betaling aanbieden en statussen opvolgen

```js
const appId = encodeURIComponent(process.env.SOFTWARE_BILLING_APPLICATION_ID);
const externalId = encodeURIComponent(accountId);
const { subscription } = await billingRequest(
  `applications/${appId}/subscriptions/by-external-id/${externalId}`,
);
const currentCycle = subscription?.cycles.find(
  (cycle) => cycle.periodStart === subscription.currentPeriodStart,
);
const paymentUrl = currentCycle?.paymentUrl ?? null;
```

Deze opvraag geeft maximaal de 20 recentste periodes terug. Toon de `paymentUrl` van de te betalen periode zoals de verkoper die terugstuurt. Bouw die link niet opnieuw op met het domein van uw toepassing. Is hij null voor een betalende periode, controleer dan de configuratie van `BOOKELIO_WEB_URL` bij de verkoper. Een gratis plan of proefperiode heeft geen betaalverzoek.

| Status | Gevolg voor de integratie |
| --- | --- |
| `TRIALING` | Proefperiode loopt; controleer `access`, de sleutels en `validUntil` |
| `ACTIVE` | Gratis of betaalde periode; blijf de berekende rechten controleren |
| `PAST_DUE` | Betaling wordt verwacht; tijdelijke toegang is mogelijk tijdens respijt, te controleren via `access`, `isInGracePeriod` en `validUntil` |
| `CANCELED` | Abonnement opgezegd; dit abonnement verleent geen functies |

De worker verwerkt verlengingen elke minuut. Aan het einde van een proefperiode of een betaalde periode maakt hij het volgende betaalverzoek aan. Een onbetaald verzoek blokkeert de aanmaak van volgende verzoeken. Verlopen rechten kunnen al geweigerd worden vóór de volgende uitvoering van de worker: verleen geen extra tijd op basis van een oude status.

De klant betaalt via de betaalmethoden van de verkoper, of de verkoper registreert een externe betaling. Geverifieerde bevestigingen koppelen de betaling aan het verzoek; een volledige betaling start standaard de factuur of hergebruikt de factuur die volgens de verkopersinstelling al bij aanmaak van het verzoek werd uitgereikt. Een geslaagde browseromleiding bewijst niet dat er betaald is: lees de rechten opnieuw op de server. Verlengingen gebruiken betaalverzoeken en geen automatische terugkerende afschrijving. [Gedeeltelijke betalingen en terugbetalingen](/docs/nl/betalingen) kunnen het recht op toegang voor een volledig betaalde periode doen vervallen.

## Het facturatiemoment instellen

Deze instelling geldt voor de verkopende organisatie, niet voor het plan of de aangesloten toepassing. Ze is beschikbaar onder **Instellingen → Facturatie → Facturen voor softwareabonnementen** en via `admin.invoicing.settings.get` / `.update`. De REST-routes zijn `GET` en `PUT /api/v1/admin/invoicing/settings/automation`: ze gebruiken niet het voorvoegsel `software-billing` van de eerdere hulpfunctie.

| `softwareSubscriptionInvoiceTiming` | Werking |
| --- | --- |
| `PAYMENT_COMPLETED` | Standaard: generatie na geverifieerde volledige betaling |
| `PAYMENT_REQUEST_CREATED` | Generatie bij aanmaak van het betaalverzoek, ook wanneer het nog onbetaald is |

Het veld is optioneel in V1-r28. Weglaten bij een wijziging behoudt de bestaande keuze; zonder verkopersconfiguratie geldt `PAYMENT_COMPLETED`. De `PUT`-bewerking vereist ook de booleans `bookings`, `trainings` en `trainingPaymentReminderInvoices`: lees de instellingen met `GET` en stuur hun huidige waarden samen met de nieuwe `softwareSubscriptionInvoiceTiming` terug, zodat deze onafhankelijke automatiseringen niet worden gewijzigd. Het activeren van generatie bij aanmaak controleert de e-mail- en opslagdiensten van de verkoper; een ontbrekende voorwaarde verhindert het opslaan.

Bookelio legt de keuze vast in elke nieuwe betalende periode. Ze geldt dus voor nieuwe abonnementen, het einde van proefperiodes, verlengingen en hervattingen die een periode aanmaken, maar niet voor bestaande verzoeken. Een gratis periode of proefperiode zonder betaalverzoek maakt geen factuur aan. Generatie bij aanmaak gebeurt nadat de abonnementstransactie is vastgelegd; de worker probeert fouten opnieuw, en uw toepassing mag het abonnement daarom niet opnieuw aanmaken.

Bij generatie op het aanmaakmoment vallen de uitgifte- en vervaldatum beide samen met de aanmaakdatum van het betaalverzoek: de factuur is bij uitgifte verschuldigd, niet op `graceEndsAt`. Deze datums verschuiven niet bij nieuwe pogingen. De aanmaak- en betalingstriggers hergebruiken dezelfde factuur: latere betaling maakt geen duplicaat aan en vervangt de bestaande PDF/XML niet. Raadpleeg het gekoppelde verzoek voor de actuele betaalstatus.

Een factuur uitreiken maakt geen betaling aan en verandert `access` niet. De respijtperiode van het plan en de geverifieerde nettobetaling blijven onafhankelijk van het facturatiemoment. Dezelfde regel geldt wanneer Bookelio zelf via een URL-verbinding klant is van de verkopende instantie.

## Gebruikersplaatsen synchroniseren en van plan veranderen

Definieer voor een externe toepassing wat een gebruikersplaats is en bereken het aantal op uw server. Bookelio volgt de gebruikers van uw toepassing niet automatisch en maakt op basis van dat aantal geen aanmeldingsquotum aan.

```js
await billingRequest(
  `applications/${appId}/subscriptions/by-external-id/${externalId}/seats`,
  { method: 'PUT', body: { seats: 12 } },
);

await billingRequest(`subscriptions/${encodeURIComponent(subscriptionId)}`, {
  method: 'PATCH',
  body: { planId: nextPlanId, seats: 12 },
});
```

Zonder `change` vullen deze wijzigingen `pendingSeats` en `pendingPlanId` voor de volgende verlenging in. Ze herberekenen de prijs of rechten van de huidige periode niet en passen geen proratering toe. Het aantal dat Bookelio bij het begin van de nieuwe periode kent, wordt vastgelegd. Wijzig ook de gebruikersplaatsen van een vast plan als u het aantal gebruikers wilt bijhouden: bij een prijs per gebruikersplaats van nul blijft het bedrag gelijk.

Om de tarieven, functies of respijtperiode van een plan voor toekomstige periodes te wijzigen, gebruikt u `PATCH /plans/{planId}` met de te wijzigen velden, bijvoorbeeld `{"basePriceCents": 450000, "gracePeriodDays": 7}`. Bestaande periodes behouden hun voorwaarden. Een softwaretoepassing of plan op `isActive: false` zetten verhindert nieuwe abonnementen ervoor; bestaande contracten worden daardoor niet opgezegd.

## Een onmiddellijke wijziging met prorata toepassen

V1-r29 voegt optionele `change` toe aan beide bovenstaande bewerkingen. `effectiveAt: 'NEXT_PERIOD'` behoudt de planning en aanvaardt alleen `proration: 'NONE'`. Verstuur `effectiveAt: 'IMMEDIATE'` met een vaste idempotentiesleutel van 1–100 tekens om nu te handelen. De standaardwaarde van `proration` is `NONE`: kies de gewenste financiële verwerking expliciet.

```js
await billingRequest(`subscriptions/${encodeURIComponent(subscriptionId)}`, {
  method: 'PATCH',
  body: {
    planId: upgradedPlanId,
    seats: 12,
    change: {
      effectiveAt: 'IMMEDIATE',
      proration: 'CHARGE_AND_CREDIT',
      idempotencyKey: 'example-account-change-2026-09-08-001',
    },
  },
});
```

Voeg voor alleen gebruikersplaatsen hetzelfde `change`-object toe aan de body `{ seats: 12 }` van `PUT .../seats`. Leid account, aantal en toegestaan plan altijd op de server af. Bewaar de sleutel voor herhalingen van dezelfde opdracht; een nieuwe sleutel betekent een nieuwe actie. Een sleutel met andere parameters hergebruiken geeft een conflict. Genereer niet voor elke netwerkpoging een nieuwe sleutel.

| `proration` | Gevolg voor de resterende periode |
| --- | --- |
| `NONE` | Past plan/gebruikersplaatsen toe zonder financiële aanpassing |
| `CHARGE_ONLY` | Factureert alleen verhogingen; geen korting bij verlagingen |
| `CHARGE_AND_CREDIT` | Factureert verhogingen en draagt toegestane gefinancierde verlagingen over als commerciële korting |

De berekening omvat het vaste bedrag en de gebruikersplaatsen, op basis van het exacte aandeel resterende UTC-tijd in de kalenderperiode, afgerond op centen. Ze gaat niet uit van maanden van 30 dagen. Halverwege een periode geeft een fictieve verhoging van € 100 naar € 160 exclusief btw € 30 vóór btw; wijzigingen aan plaatsen beïnvloeden een plan met `seatPriceCents: 0` niet.

Het abonnement mag niet opgezegd zijn, de periode moet nog geldig zijn en alle financiële verplichtingen moeten voldaan zijn. Een onbetaald bedrag blokkeert de aanpassing, ook tijdens respijt. De nieuwe facturatiefrequentie moet gelijk blijven; plan overgangen tussen maandelijks en jaarlijks bij de verlenging. Tijdens een proefperiode of aanbieding behoudt de wijziging de gratis einddatum en ontstaat geen verschuldigd bedrag of krediet. Laat `change` weg of gebruik `NEXT_PERIOD` zonder prorata om een opgezegd abonnement opnieuw te activeren.

De server voegt een onveranderlijke periode toe waarvan `previousCycleId` de voorganger aanwijst. `currentPeriodStart` wordt de wijzigingsdatum, maar het oorspronkelijke einde en de verlengingsankerdatum blijven gelijk. Eerdere facturen en verzoeken blijven intact. Een nog te betalen toeslag maakt een verzoek met vastgelegde factuur- en respijtregels voor deze nieuwe periode; de rechten volgen betaling of dat respijt. Lees `entitlements` opnieuw na de aanroep: een geslaagde wijziging bewijst geen betaling en schrijft niet automatisch geld van een kaart af.

`creditGrantedCents` registreert de toegekende korting, `creditAppliedCents` de verbruikte korting en `creditBalanceCents` het resterende saldo, in centen exclusief btw voor alleen dit abonnement. Het saldo vermindert toekomstige bedragen vóór btw: het is geen boekhoudkundige creditnota op een eerdere factuur, bankterugbetaling of betaling. Er ontstaat geen negatief verzoek of fictieve betaling. De korting is begrensd tot de gefinancierde waarde; een proefperiode, aanbieding of gratis verhoging levert geen crediteerbare waarde op. Algemene terugbetalingen van een abonnement met aanpassingen worden geweigerd totdat een specifieke reconciliatieprocedure beschikbaar is.

## Gratis maanden toekennen via de API

Voeg bij aanmaak bijvoorbeeld `offer: { label: 'Welkomstaanbieding', freeMonths: 1 }` toe aan de body van `POST /subscriptions`. Gebruik voor een bestaand abonnement `subscriptions.grantOffer`:

```js
await billingRequest(
  `subscriptions/${encodeURIComponent(subscriptionId)}/offers`,
  {
    method: 'POST',
    body: {
      label: 'Eén gratis maand — fictief voorbeeld',
      freeMonths: 1,
      idempotencyKey: 'example-commercial-offer-001',
    },
  },
);
```

De omschrijving telt 1–200 tekens, `freeMonths` is een geheel getal van 1 tot 36 en de sleutel telt 1–100 tekens. Er mag maar één aanbieding wachten. Dezelfde sleutel en gegevens opnieuw versturen voegt geen maanden toe, ook niet na toepassing; andere gegevens onder die sleutel geven een conflict. De aanmaak van een bestaand abonnement herhalen voegt geen aanbieding toe: gebruik de specifieke bewerking. Een opgezegd abonnement moet eerst opnieuw geactiveerd worden.

Een initiële aanbieding bij een herhaalde aanmaak moet overeenkomen met de reeds geregistreerde aanbieding; een ontbrekende of andere aanbieding op het bestaande abonnement geeft een conflict. Antwoorden tonen maximaal de 20 recentste aanbiedingen, net als de 20 recente periodes: dit is geen volledige export van de historiek.

De aanbieding wacht op de volgende normaal betalende periode, na de proefperiode of reeds uitgereikte periode; zonder proefperiode kan ze bij aanmaak beginnen. Onbetaalde bedragen blijven blokkerend en volledig gratis plannen laten ze wachten. Geen bestaand document of verschuldigd bedrag verandert. Eén maand betekent één UTC-kalendermaand, ook bij een jaarplan, met aanpassing aan het maandeinde; de betalende facturatie hervat aan het einde van de aanbieding met die nieuwe ankerdatum.

De aangeboden periode toont `isComplimentary: true` en `offerId`, blijft `ACTIVE` en maakt geen betaalverzoek of automatische factuur aan. Het is geen nieuwe proefperiode; controleer altijd `access` en `validUntil`. `offers` toont de omschrijving, duur, `appliedAt` en periodedatums zodra ze verbruikt is. De nieuwe uitvoervelden voor prorata en aanbiedingen zijn optioneel om oudere verkopers te aanvaarden: hun afwezigheid bewijst geen korting of aanbieding. De verkoper moet V1-r29 gebruiken om deze opties toe te passen; controleer zijn revisie voordat u deze acties aanbiedt.

Bij vertraging van de worker begint een aanbieding nooit vóór de toekenningsdatum. Als de volledige gratis periode al verstreken zou zijn vóór verwerking, begint ze bij het hervatten van de worker zodat ze niet volledig in het verleden wordt verbruikt. De datums die Bookelio terugstuurt zijn bepalend.

## Opzeggen en opnieuw activeren

```js
await billingRequest(
  `subscriptions/${encodeURIComponent(subscriptionId)}/cancel`,
  { method: 'POST', body: { cancelAtPeriodEnd: true } },
);
```

`true` plant het einde bij het aflopen van de periode: reeds geldige rechten blijven binnen hun huidige grens bruikbaar, zonder een respijtperiode te verlengen die vóór die datum afloopt. `false` vraagt om onmiddellijke opzegging en beëindigt ook lopend respijt; het betekent niet dat een geplande opzegging wordt geannuleerd. Uitgegeven betaalverzoeken en de financiële historiek blijven behouden.

Zodra de status `CANCELED` is, verstuurt u een `PATCH /subscriptions/{subscriptionId}` met een expliciete `planId` om opnieuw te activeren. Alleen de gebruikersplaatsen synchroniseren volstaat niet. Een reeds betaalde en nog geldige periode wordt hervat zonder opnieuw gefactureerd te worden; na afloop begint een nieuwe periode. Hervatten geeft geen nieuwe proefperiode en wordt geweigerd als er nog een oud onbetaald verzoek is. `POST /subscriptions` opnieuw uitvoeren vindt het bestaande abonnement terug en activeert het niet opnieuw.

Het plan wijzigen via `subscriptions.update` voordat de geplande opzegging ingaat, verwijdert `cancelAtPeriodEnd` niet. Sinds V1-r33 past expliciete activering via `subscriptions.activate`, hieronder beschreven, het plan nu toe en verwijdert ze die planning na controle van openstaande bedragen.

## Bookelio via een URL met een facturatie-instantie verbinden

Deze configuratie geldt voor Bookelio zelf. Maak op de verkopende instantie een Bookelio-softwaretoepassing aan en bereid uw plannen voor. Bij het opstarten voegt Bookelio ontbrekende sleutels uit zijn catalogus toe, bijvoorbeeld `invoicing`, `trainings` of `softwareBilling`, zonder bestaande definities te overschrijven. Selecteer daarna expliciet de functies in elk plan: een sleutel toevoegen maakt die niet automatisch beschikbaar in aanbiedingen. Zo kan de module voor abonnementsverkoop zelf ook deel uitmaken van een aanbod.

Configureer op **de API en de workers van de klantinstantie** de drie variabelen die Bookelio herkent:

```dotenv
BOOKELIO_BILLING_URL=https://api.billing.example.com
BOOKELIO_BILLING_API_KEY=REPLACE_WITH_SELLER_ORGANIZATION_KEY
BOOKELIO_BILLING_APPLICATION_ID=REPLACE_WITH_BOOKELIO_APPLICATION_ID
```

De verbinding verloopt altijd via HTTP `/rpc/v1`, ook als de URL naar dezelfde instantie verwijst. Het adres moet bereikbaar zijn vanaf de API en de workers. Gebruik een API-URL zonder ingebedde aanmeldgegevens, queryparameters of fragment. Op de API en de workers van de verkopende instantie verwijst `BOOKELIO_WEB_URL` naar de origin van het publieke dashboard voor betaallinks. De verbindingssleutel heeft `softwareBilling.read`, `softwareBilling.manage` en `customers.create` nodig voor raadpleging, registratie en synchronisatie; voeg `paymentRequests.create` toe om organisaties op plannen in te schrijven.

### Organisaties koppelen bij het opstarten

Na het openen van haar HTTP-server registreert de klant-API de ontbrekende catalogus en accounts voor de lokale niet-legacyorganisaties. Ze gebruikt de organisatie-ID als `externalId`, de naam en het aantal leden, inclusief de eigenaar, met minstens één gebruikersplaats. Een externe koppeling is uniek voor de toepassing en dit identificatiegegeven. Herhalingen hergebruiken de koppeling; een scan zonder overlap probeert fouten opnieuw en ontdekt elke minuut nieuwe organisaties. Legacyorganisaties worden overgeslagen voor accounts en synchronisatie van gebruikersplaatsen, maar niet voor de registratie van de gedeelde catalogus. Het opstarten wacht niet op een antwoord van de eigen URL voordat de server begint te luisteren.

Als voor deze identiteit al een extern abonnement bestaat, wordt de klant ervan hergebruikt. Anders maken lokale bedrijfsgegevens alleen een facturatieklant aan als het vereiste profiel volledig en geldig is. Er wordt geen land of fiscaal identificatiegegeven verzonnen. Een onvolledig profiel laat een gekoppeld account zonder klant achter; gegevens van een al gekoppelde klant worden niet overschreven. De bewerking `applications.provision` zelf maakt geen plan, abonnement, betaalverzoek, factuur of betaling aan.

Als er al een abonnement bestaat, roept de scan na deze bewerking `subscriptions.syncSeats` aan zonder onmiddellijke optie: gebruikersplaatsen worden bij verlenging gepland, zonder prorata of nieuwe betaalverplichting. Een opgezegd abonnement wordt niet opnieuw geactiveerd. Deze aanroep behoudt bestaande automatisering: ze kan een al verwachte factuur voor een bestaand betaalverzoek in de modus `PAYMENT_REQUEST_CREATED` opnieuw proberen, zonder een nieuw verzoek aan te maken.

De verkoper moet V1-r30 gebruiken en het aanvullende schema hebben uitgerold voordat dit opstartproces bij de klant wordt ingeschakeld. Een oudere instantie kan bestaande leesaanvragen nog beantwoorden, maar biedt de nieuwe registratiebewerking niet aan. De sleutel moet beide registratierechten hebben, ook als een batch uiteindelijk geen nieuwe klant nodig heeft.

De bewerking is ook beschikbaar voor een andere toepassing: `applications.provision`, of `POST /applications/{applicationId}/provision` onder het REST-voorvoegsel van de module. Fictief voorbeeld zonder facturatieprofiel, dus zonder klantaanmaak:

```js
const provisioned = await billingRequest(
  `applications/${encodeURIComponent(applicationId)}/provision`,
  {
    method: 'POST',
    body: {
      features: [{ key: 'reporting', name: 'Rapporten' }],
      accounts: [{ externalId: 'example-organisation-001', name: 'Voorbeeldorganisatie', seats: 5 }],
    },
  },
);
```

Batches aanvaarden maximaal 200 functies en 100 accounts. Het antwoord identificeert de toepassing, sleutels en voor elke identiteit `accountId`, `customerId` en `subscriptionId`; de laatste twee kunnen `null` zijn. Een geregistreerde identiteit bewijst dus geen abonnement of toegang. Gebruik altijd `entitlements` om functies toe te staan. Een identieke aanroep dupliceert geen sleutels of accounts en vervangt geen bestaande functiemetadata.

### Plannen toewijzen en externe rechten toepassen

In de superadmin controleert de pagina **Facturatie** de verbinding. Het abonnementstabblad van een organisatie kan een plan en een klant van de verkopende instantie koppelen. De eigenaar kan na bevestiging ook een plan kiezen via **Instellingen → Mijn Bookelio-abonnement**. Bookelio gebruikt de ID van de klantorganisatie als `externalId` en synchroniseert het aantal leden, inclusief de eigenaar, met een minimum van één gebruikersplaats.

De verbinding gebruikt dezelfde berekende rechten, inclusief de respijtperiode op het plan van de verkoper. Er is geen extra respijtinstelling nodig op de klantinstantie, ook niet wanneer die haar eigen URL voor facturatie gebruikt.

Aanbiedingen en aanpassingen bij de verkoper gebruiken ook deze verbinding. De automatische synchronisatie van Bookelio-leden stuurt geen onmiddellijke optie: wijzigingen aan gebruikersplaatsen blijven gepland bij verlenging. Uw eigen abonnementspagina toont aanbiedingen en het kortingssaldo alleen ter inzage; commerciële wijzigingen gebeuren bij de verkoper.

Voor niet-legacyorganisaties bepaalt de Bookelio-leverancier alle functies, ook functies die eerder standaard of zonder tarief waren ingeschakeld. Een sleutel die ontbreekt in zijn rechten is uitgeschakeld. Sinds V1-r32 gebruiken Bookelio-plannen uitsluitend een externe leverancier: oude lokale plannen en abonnementen, standaardwaarden en individuele uitzonderingen verlenen geen toegang meer. De lokale catalogus behoudt definities, sleutelkoppelingen en globale activering, geen commerciële rechten. Historische tabellen worden niet verwijderd; de verkoopmodule blijft haar eigen plannen opslaan, ook op dezelfde instantie via HTTP gelezen.

De generieke leverancier `BOOKELIO_FEATURE_PROVIDER_*` wordt niet naast Bookelio bevraagd. Een onvolledige Bookelio-configuratie of storing activeert geen terugval. Als de drie variabelen `BOOKELIO_BILLING_*` ontbreken, kan alleen de generieke leverancier rechten leveren; zonder enige leverancier heeft een niet-legacyorganisatie geen functies. Die generieke leverancier biedt alleen een leescontract: Bookelio verzint geen schrijf-API om er sleutels of organisaties aan te maken. De hier beschreven registratie geldt uitsluitend voor een Bookelio-instantie die via `BOOKELIO_BILLING_*` is ingesteld.

### Expliciete vrijstelling voor legacyorganisaties

V1-r32 voegt `Organization.bookelioLegacyAccess` toe, standaard `false`. Alleen een superbeheerder kan een organisatie als legacy markeren, bijvoorbeeld voor Bookelio zelf of een contract voor ontwikkeling op maat. De geauditeerde bewerking `superadmin.organizations.setLegacyAccess` aanvaardt strikt `{ organizationId, enabled }` en antwoordt met `{ id, bookelioLegacyAccess }`. De REST-route is `PUT /api/v1/superadmin/organizations/{organizationId}/legacy-access`. Een eigenaar of organisatiesleutel kan ze niet aanroepen; geen generieke variabele of uitzondering per functie verleent deze vrijstelling.

Legacy geeft toegang tot alle globaal actieve functies zonder de leverancier te raadplegen, ook als die ontbreekt, fout geconfigureerd of onbereikbaar is. Het omzeilt noch authenticatie, organisatie-isolatie, bedrijfsrechten van leden en sleutels, noch een globale uitschakeling van een functie. Diagnostiek toont het optionele object `providers.legacy: { active: true }` en behoudt de V1-bron `manual`. Oude uitzonderingsroutes blijven beschikbaar voor compatibiliteit, maar hun gegevens verlenen geen rechten meer.

De leesbewerkingen `admin.organization.subscription.get/options` tonen de optionele boolean `bookelioLegacyAccess`. Voor legacy antwoorden ze met `connected: false`, zonder extern leesverzoek, met `subscription: null` voor `get` en geen plannen voor `options`. Dit resultaat betekent een vrijstelling en geen leveranciersstoring. Mijn Bookelio-abonnement legt dat uit zonder plankeuze; `configure` weigert een plan voor een legacyorganisatie.

De vrijstelling inschakelen annuleert geen bestaand abonnement bij de verkoper, stopt geen toekomstige facturen en verwijdert geen openstaande schuld of documenten. Beheer dat abonnement apart en expliciet bij de verkoper. Legacy uitschakelen herstelt de externe controles; synchronisatie kan hervatten zonder automatisch een abonnement aan te maken.

Laat vóór de uitrol van V1-r32 de aanvullende kolom `Organization.bookelioLegacyAccess Boolean @default(false)` handmatig genereren, nakijken en toepassen. Bereid externe rechten voor of markeer tijdens de omschakeling expliciet alleen de bedoelde legacyorganisaties. Markeer bestaande klanten niet automatisch en verwijder geen historische tabellen: zonder leverancier krijgt een niet-legacyorganisatie haar eerdere lokale rechten niet meer.

### De eigenaar kiest een plan via de klant-API

V1-r31 voegt twee bewerkingen toe op de API van de klantinstantie, niet die van de verkoper. Ze vereisen een menselijke sessie en een `owner`-lidmaatschap dat nog in de database bestaat voor de actieve organisatie. Een API-sleutel, beheerder zonder eigenaarschap of andere organisatie kan ze niet gebruiken. De controle hangt niet af van de verkoopmodule, zodat verlopen toegang hersteld kan worden. Andere bevoegde rollen behouden de bestaande leesbewerking `admin.organization.subscription.get`.

| oRPC-bewerking | REST-route | Effect |
| --- | --- | --- |
| `admin.organization.subscription.options` | `GET /api/v1/admin/organization/subscription/options` | Alleen lezen: `connected`, actieve `plans`, serverberekende `seats` en `billingDetailsRequired` |
| `admin.organization.subscription.configure` | `PUT /api/v1/admin/organization/subscription/plan` | Na bevestiging: kiezen met `{ planId, expectedOrganizationId, activation? }`, met het Bookelio-abonnement als antwoord |

`expectedOrganizationId` vermeldt de organisatie waarvoor de eigenaar net heeft bevestigd. Het is alleen een voorwaarde: als de sessie ondertussen van organisatie is veranderd, weigert de API met `CONFLICT` vóór enige schrijfactie. Het doel, de externe identiteit en de plaatsen blijven uit de sessie en database afgeleid; extra prijzen, klanten, aantallen, aanbiedingen of prorataparameters worden niet aanvaard.

De opties lezen maakt geen klant of abonnement aan. Bij configuratie hergebruikt de server de klant van het externe abonnement of account. Het verkopersresultaat `subscriptions.getByExternalId` bevat voortaan een optioneel `account` voor deze koppeling vóór het abonnement; oude antwoorden zonder dat veld blijven aanvaard. Zonder gekoppelde klant wordt een volledig bedrijfsprofiel aan de bestaande registratie doorgegeven; onvolledige gegevens vereisen het aanvullen van **Instellingen → Bedrijfsgegevens** vóór bevestiging. Er wordt geen land of fiscaal identificatiegegeven verzonnen en een al gekoppelde klant wordt niet herschreven.

Sinds V1-r33 stuurt het dashboard `activation: { idempotencyKey }` om het plan onmiddellijk toe te passen. De sleutel blijft gelijk bij nieuwe pogingen voor dezelfde bevestiging. Zonder dit optionele veld behouden oudere aanroepen hun gedrag: eerste aanmaak, een nieuw plan bij verlenging of beschermde hervatting. Een eerste keuze volgt de regels voor gratis gebruik en een proefperiode; een latere keuze begint nooit een nieuwe proefperiode.

De server roept vervolgens `admin.softwareBilling.subscriptions.activate` bij de verkoper aan (`POST /api/v1/admin/software-billing/subscriptions/activate`) met de strikte invoer `{ applicationId, externalId, customerId, planId, seats, idempotencyKey }`. Deze atomaire bewerking vereist `softwareBilling.manage` en `paymentRequests.create` en bewaart een ontvangstbewijs voor elke geslaagde activering, ook als het plan al toegepast is. De sleutel en vingerafdruk voorkomen dubbele aanrekeningen of het opnieuw toepassen van een oude keuze na een latere wijziging. De verkoper berekent prijzen en financiële verwerking; die worden nooit in de browser gekozen.

Het beleid is `CHARGE_ONLY`: bij dezelfde facturatiefrequentie wordt alleen een positief verschil naar rato aangerekend voor de resterende tijd tot de oorspronkelijke einddatum. Een gewijzigde frequentie begint nu een nieuwe periode en trekt de resterende waarde exclusief btw af van de dienst waarvan de financiering geverifieerd is; een overschot wordt niet overgedragen of terugbetaald. Een proefperiode of aangeboden periode behoudt haar gratis einddatum, ook bij een andere frequentie. Een verlopen, betaalde periode begint nu opnieuw zonder oude periodes achteraf aan te rekenen. Expliciete activering verwijdert ook een al geplande opzegging.

Een keuze die overeenkomt met de huidige momentopname, zonder geplande wijzigingen, geeft een bestaand betaalverzoek terug zonder extra aanrekening. Openstaande verplichtingen verhinderen een andere configuratie. Het huidige plan opnieuw toepassen kan verouderde functiemomentopnames vernieuwen, bijvoorbeeld die van een oudere gratis periode. Eerdere periodes, betalingen en facturen blijven intact. Lees het abonnement en de rechten opnieuw: de pagina biedt **Nu betalen** aan voor een verschuldigd verzoek, maar een plan bevestigen schrijft niet automatisch geld van een kaart af en bewijst geen betaling. Betalende functies volgen op geverifieerde betaling of toepasselijke respijt. Aanbiedingen blijven beslissingen van de verkoper.

De serversleutel naar de verkoper behoudt de rechten `softwareBilling.read`, `softwareBilling.manage`, `customers.create` en `paymentRequests.create`. Hij wordt nooit naar de browser gestuurd. Aanroepen blijven HTTP `/rpc/v1`, ook voor dezelfde instantie. Laat de aanvullende tabel `BookelioSoftwareActivation`, relatie en unieke sleutel handmatig genereren, nakijken en toepassen vóór de uitrol van verkoper V1-r33 en daarna de klant die activering vraagt. Een oudere verkoper krijgt geen uitgestelde wijziging als terugval. De bestaande aanvullende periode- en accountschema's en legacykolom blijven vereist; verzin geen historische ontvangstbewijzen en voeg geen extra omgevingsvariabele toe. Zonder verbinding of voor een legacyorganisatie kan via dit traject geen plan gekozen worden.

Vóór activering door de eigenaar leest de server het openbare register `/api-versions.json` van de verkoper, zonder de API-sleutel mee te sturen. Een V1-vermelding met revisie 33 of hoger is vereist vóór enige accountregistratie of activering. Een oudere verkoper kan nog plannen tonen, maar de activering door de eigenaar wordt geweigerd met een fout `SERVICE_UNAVAILABLE` die om een update vraagt; dit betekent niet dat het plan of account verdwenen is. Als het register onbereikbaar of ongeldig is, meldt de fout dat de versie niet gecontroleerd kan worden: herstel dat toegangspunt vóór een nieuwe poging. Oudere aanroepen zonder `activation` blijven ongewijzigd.

## De integratie onderzoeken en valideren

| Symptoom | Controle |
| --- | --- |
| Sleutel geweigerd of toegang verboden | Geldige organisatiesleutel, rechten voor de bewerking en module geactiveerd voor de verkoper |
| Functies ontbreken zonder leverancier | Configureer externe rechten of laat een superbeheerder de legacyvrijstelling nakijken; geen lokaal plan dient als terugval |
| Legacyorganisatie zonder plankeuze | Verwacht gedrag; de vrijstelling annuleert geen abonnement bij de verkoper |
| Bookelio-plankeuze geweigerd | Eigenaarssessie voor de actieve organisatie; geen API-sleutel vervangt deze controle. Na een organisatiewissel herladen vóór bevestiging |
| Gegevens gevraagd vóór inschrijving | Vul de bedrijfsgegevens aan of laat de al gekoppelde klant bij de leverancier controleren; gebruik nooit verzonnen gegevens |
| Software, plan of klant niet gevonden | Identificatiegegevens moeten bij dezelfde verkoper horen; het plan moet bij de softwaretoepassing horen |
| Koppeling bij opstarten ontbreekt | Verkoper met V1-r30, aanvullende migratie toegepast, bereikbare URL en rechten `softwareBilling.manage` / `customers.create`; wacht na correctie op de volgende scan |
| Gekoppeld account zonder abonnement of klant | Onvolledig facturatieprofiel of geen toegewezen plan; vervolledig het profiel of selecteer expliciet klant en plan, zonder automatische facturatie te verwachten |
| Conflict bij aanmaak | Externe referentie al in gebruik; lees het abonnement opnieuw en wijzig het in plaats van het opnieuw aan te maken |
| Geen rechten na terugkeer van de betaling | Volledige betaling daadwerkelijk bevestigd, periode nog geldig, functiesleutel inbegrepen |
| `PAST_DUE` terwijl de toegang nog open is | Actief respijt: controleer `isInGracePeriod` en `validUntil`, zonder die toegang als betaling te beschouwen |
| Respijt ontbreekt of is al afgelopen | Vastgelegde waarde in de periode, periodebegin, `graceEndsAt`, opzegging of terugbetaling; het plan wijzigen verandert de historiek niet |
| Nieuw plan of nieuw aantal niet zichtbaar | Controleer zonder onmiddellijke optie `pendingPlanId` en `pendingSeats`; lees anders de rechten en het nieuwe te betalen verzoek opnieuw |
| Onmiddellijke wijziging geweigerd | Geen schuld, recht `paymentRequests.create` en een consistente idempotentiesleutel; `subscriptions.update` vereist ook een geldige periode en dezelfde frequentie, `subscriptions.activate` vereist een gemigreerde V1-r33-verkoper |
| Aanbieding blijft wachten | Lopende proefperiode of uitgereikte periode, gratis plan, blokkerend onbetaald bedrag; bekijk de datums in `offers` |
| Geen betaallink | Gratis periode of proefperiode, of de publieke origin van de verkoper ontbreekt |
| Factuur verwacht bij aanmaak van het verzoek maar ontbreekt | Verkoperskeuze vastgelegd bij aanmaak van de periode, generatie- en verzenddiensten beschikbaar en worker actief; geen terugwerkend effect |
| Geen verlenging | Worker actief, periode afgelopen en geen blokkerend onbetaald verzoek |

Controleer op een testinstallatie die voor deze module is voorbereid een gratis plan, een proefperiode, een onvolledige en daarna volledige betaling, een vast plan met verschillende aantallen gebruikers en een aanbod per gebruikersplaats. Test uitgeschakeld en daarna ingeschakeld respijt, de exacte eindgrens, betaling tijdens en na het respijt, een terugbetaling en een wijziging die alleen toekomstige periodes verandert. Controleer beide factuurtriggers, het ontbreken van duplicaten na betaling en het ontbreken van terugwerkend effect op bestaande verzoeken. Controleer ook dat een account nooit de rechten of betaalverzoeken van een ander account kan raadplegen door een identificatiegegeven in de browser te wijzigen. Sluit af met een geplande wijziging, een opzegging en een hervatting zonder nieuwe proefperiode, en vervolgens met een gesimuleerde verbindingsstoring.

Test voor de V1-r29-opties een onmiddellijke verhoging en daarna verlaging, elke proratamodus, extra plaatsen en herhalingen met dezelfde sleutel. Controleer een korting die op een toekomstig bedrag wordt verbruikt en het ontbreken van krediet na een gratis upgrade. Test één gratis maand op een jaarplan, na een proefperiode en met een onbetaalde factuur, plus de weigering van een tweede wachtende aanbieding. Controleer de nieuwe acties met en zonder hun financiële rechten.

Test voor V1-r30 het opstarten met een externe URL en daarna die van dezelfde instantie, twee opeenvolgende scans, een functie die de verkoper al heeft gedefinieerd en een organisatie die na het opstarten wordt aangemaakt. Controleer volledige en onvolledige profielen, hergebruik van een bestaande klant, geen nieuw abonnement of nieuwe financiële verplichting en weigering van sleutels zonder rechten. Controleer ook geplande gebruikersplaatsen en normale herhalingen van verwachte facturen op bestaande verzoeken. Simuleer een storing: geen lokale waarde of uitzondering mag de rechten van de ingestelde leverancier omzeilen.

Controleer voor V1-r31 dat opties lezen geen klant of abonnement aanmaakt, dat een niet-eigenaar of API-sleutel geen plan kan configureren en dat wisselen van organisatie vóór bevestiging een conflict veroorzaakt. Test hergebruik van een gekoppelde klant, ontbrekende gegevens, een eerste gratis plan of proefperiode, een uitgestelde wijziging en hervatting zonder nieuwe proefperiode. De keuze mag geen door de browser aangeleverde prijzen, klanten, aantallen of commerciële opties aanvaarden.

Controleer voor V1-r32 de weigering zonder leverancier voor een niet-legacyorganisatie, ook met een oud plan of uitzondering. Test legacy met een ontbrekende of onbereikbare leverancier, een inactieve functie en een lid zonder bedrijfsrecht. Controleer dat alleen een superbeheerder de status wijzigt, dat audit wordt vastgelegd en dat voor legacy geen externe account-, plaatsen- of plankeuzeaanroep plaatsvindt. De vrijstelling verwijderen moet externe controles herstellen zonder schuld te wissen of een abonnement aan te maken.

Test voor V1-r33 de onmiddellijke activering van een gratis of betalend plan, het vernieuwen van de huidige functies, de toeslag naar rato, een gewijzigde frequentie en hervatting. Controleer het aangeboden betaalverzoek na bevestiging en herladen, de weigering van een andere configuratie bij openstaande schuld, nieuwe pogingen zonder dubbels en oudere aanroepen zonder `activation` die wijzigingen blijven plannen. Een oudere verkoper moet activering weigeren zonder stilzwijgend een wijziging bij verlenging toe te passen.

Betaalperiodes, toegangsrechten en facturen worden door Bookelio beheerd. De aangesloten toepassing gebruikt de API om ze te raadplegen en te beheren; ze wijzigt periodes, betaalstatussen of de facturatiedatabase niet rechtstreeks.
