@fleetless/contracts 6.2.0 → 6.3.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/artifacts/openapi.json +4322 -1887
  3. package/artifacts/routes.json +326 -2
  4. package/artifacts/schema/billing-address.schema.json +49 -0
  5. package/artifacts/schema/billing-cancel-request.schema.json +12 -0
  6. package/artifacts/schema/billing-change-request.schema.json +60 -0
  7. package/artifacts/schema/billing-change-response.schema.json +765 -0
  8. package/artifacts/schema/billing-details-update.schema.json +25 -0
  9. package/artifacts/schema/billing-details.schema.json +176 -0
  10. package/artifacts/schema/billing-invoice.schema.json +76 -0
  11. package/artifacts/schema/billing-view.schema.json +671 -0
  12. package/artifacts/schema/checkout-request.schema.json +252 -0
  13. package/artifacts/schema/checkout-response.schema.json +22 -0
  14. package/artifacts/schema/checkout-status.schema.json +49 -0
  15. package/artifacts/schema/payment-method-change-request.schema.json +19 -0
  16. package/artifacts/schema/payment-method.schema.json +80 -0
  17. package/artifacts/schema/payment-provider-unavailable-details.schema.json +29 -0
  18. package/artifacts/schema/vat-id-check-request.schema.json +22 -0
  19. package/artifacts/schema/vat-id-check-response.schema.json +36 -0
  20. package/dist/billing.d.ts +778 -0
  21. package/dist/billing.js +441 -0
  22. package/dist/errors.d.ts +17 -5
  23. package/dist/errors.js +31 -0
  24. package/dist/index.d.ts +4 -2
  25. package/dist/index.js +5 -1
  26. package/dist/plans.d.ts +9 -9
  27. package/dist/realtime.d.ts +2 -2
  28. package/dist/rest.d.ts +3 -3
  29. package/dist/routes.d.ts +1 -1
  30. package/dist/routes.js +181 -4
  31. package/package.json +1 -1
package/dist/plans.d.ts CHANGED
@@ -29,8 +29,8 @@ export declare const PLAN_ORDER: readonly PlanId[];
29
29
  */
30
30
  export declare const planLimitKey: z.ZodEnum<{
31
31
  apps: "apps";
32
- robots: "robots";
33
32
  seats: "seats";
33
+ robots: "robots";
34
34
  app_users: "app_users";
35
35
  live_video_ms_per_month: "live_video_ms_per_month";
36
36
  asset_bytes_per_robot: "asset_bytes_per_robot";
@@ -139,8 +139,8 @@ export type PlanCatalogueEntry = z.infer<typeof planCatalogueEntry>;
139
139
  */
140
140
  export declare const addonKey: z.ZodEnum<{
141
141
  apps: "apps";
142
- robots: "robots";
143
142
  seats: "seats";
143
+ robots: "robots";
144
144
  app_user_packs: "app_user_packs";
145
145
  live_video_packs: "live_video_packs";
146
146
  }>;
@@ -148,15 +148,15 @@ export type AddonKey = z.infer<typeof addonKey>;
148
148
  export declare const addonCatalogueEntry: z.ZodObject<{
149
149
  key: z.ZodEnum<{
150
150
  apps: "apps";
151
- robots: "robots";
152
151
  seats: "seats";
152
+ robots: "robots";
153
153
  app_user_packs: "app_user_packs";
154
154
  live_video_packs: "live_video_packs";
155
155
  }>;
156
156
  raises: z.ZodEnum<{
157
157
  apps: "apps";
158
- robots: "robots";
159
158
  seats: "seats";
159
+ robots: "robots";
160
160
  app_users: "app_users";
161
161
  live_video_ms_per_month: "live_video_ms_per_month";
162
162
  asset_bytes_per_robot: "asset_bytes_per_robot";
@@ -260,8 +260,8 @@ export type PlanChangeKeep = z.infer<typeof planChangeKeep>;
260
260
  * is locked (see `orgLock`) and must land on Basic.
261
261
  */
262
262
  export declare const planChangeReason: z.ZodEnum<{
263
- cancel: "cancel";
264
263
  downgrade: "downgrade";
264
+ cancel: "cancel";
265
265
  migration: "migration";
266
266
  lock: "lock";
267
267
  }>;
@@ -292,8 +292,8 @@ export declare const pendingPlanChange: z.ZodObject<{
292
292
  enterprise: "enterprise";
293
293
  }>;
294
294
  reason: z.ZodEnum<{
295
- cancel: "cancel";
296
295
  downgrade: "downgrade";
296
+ cancel: "cancel";
297
297
  migration: "migration";
298
298
  lock: "lock";
299
299
  }>;
@@ -391,8 +391,8 @@ export declare const orgPlan: z.ZodObject<{
391
391
  enterprise: "enterprise";
392
392
  }>;
393
393
  reason: z.ZodEnum<{
394
- cancel: "cancel";
395
394
  downgrade: "downgrade";
395
+ cancel: "cancel";
396
396
  migration: "migration";
397
397
  lock: "lock";
398
398
  }>;
@@ -455,8 +455,8 @@ export type PlanChangeRequest = z.infer<typeof planChangeRequest>;
455
455
  */
456
456
  export declare const planOverrides: z.ZodObject<{
457
457
  apps: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
458
- robots: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
459
458
  seats: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
459
+ robots: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
460
460
  app_users: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
461
461
  live_video_ms_per_month: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
462
462
  asset_bytes_per_robot: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -490,8 +490,8 @@ export declare const adminPlanChangeRequest: z.ZodObject<{
490
490
  }, z.core.$strict>>;
491
491
  overrides: z.ZodOptional<z.ZodObject<{
492
492
  apps: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
493
- robots: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
494
493
  seats: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
494
+ robots: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
495
495
  app_users: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
496
496
  live_video_ms_per_month: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
497
497
  asset_bytes_per_robot: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -260,13 +260,13 @@ export type DatapointEvent = z.infer<typeof datapointEvent>;
260
260
  */
261
261
  export declare const liveSessionEndReason: z.ZodEnum<{
262
262
  unknown: "unknown";
263
+ expired: "expired";
263
264
  plan_limit: "plan_limit";
264
265
  publish_failed: "publish_failed";
265
266
  robot_offline: "robot_offline";
266
267
  config_changed: "config_changed";
267
268
  released_by_peer: "released_by_peer";
268
269
  revoked: "revoked";
269
- expired: "expired";
270
270
  robot_deleted: "robot_deleted";
271
271
  }>;
272
272
  export type LiveSessionEndReason = z.infer<typeof liveSessionEndReason>;
@@ -292,13 +292,13 @@ export declare const liveSessionEvent: z.ZodObject<{
292
292
  state: z.ZodLiteral<"ended">;
293
293
  reason: z.ZodEnum<{
294
294
  unknown: "unknown";
295
+ expired: "expired";
295
296
  plan_limit: "plan_limit";
296
297
  publish_failed: "publish_failed";
297
298
  robot_offline: "robot_offline";
298
299
  config_changed: "config_changed";
299
300
  released_by_peer: "released_by_peer";
300
301
  revoked: "revoked";
301
- expired: "expired";
302
302
  robot_deleted: "robot_deleted";
303
303
  }>;
304
304
  detail: z.ZodNullable<z.ZodString>;
package/dist/rest.d.ts CHANGED
@@ -1847,10 +1847,10 @@ export declare const USAGE_WINDOW_MAX_DAYS = 366;
1847
1847
  * told to look at the wrong thing.
1848
1848
  */
1849
1849
  export declare const usageMetric: z.ZodEnum<{
1850
+ asset_bytes: "asset_bytes";
1850
1851
  api_calls: "api_calls";
1851
1852
  live_session_ms: "live_session_ms";
1852
1853
  retention_bytes: "retention_bytes";
1853
- asset_bytes: "asset_bytes";
1854
1854
  robot_online_ms: "robot_online_ms";
1855
1855
  }>;
1856
1856
  export type UsageMetric = z.infer<typeof usageMetric>;
@@ -1970,10 +1970,10 @@ export declare const usageRow: z.ZodObject<{
1970
1970
  app_id: z.ZodNullable<z.ZodUUID>;
1971
1971
  app_name: z.ZodNullable<z.ZodString>;
1972
1972
  metric: z.ZodEnum<{
1973
+ asset_bytes: "asset_bytes";
1973
1974
  api_calls: "api_calls";
1974
1975
  live_session_ms: "live_session_ms";
1975
1976
  retention_bytes: "retention_bytes";
1976
- asset_bytes: "asset_bytes";
1977
1977
  robot_online_ms: "robot_online_ms";
1978
1978
  }>;
1979
1979
  day: z.ZodString;
@@ -1986,10 +1986,10 @@ export declare const orgUsageResponse: z.ZodObject<{
1986
1986
  app_id: z.ZodNullable<z.ZodUUID>;
1987
1987
  app_name: z.ZodNullable<z.ZodString>;
1988
1988
  metric: z.ZodEnum<{
1989
+ asset_bytes: "asset_bytes";
1989
1990
  api_calls: "api_calls";
1990
1991
  live_session_ms: "live_session_ms";
1991
1992
  retention_bytes: "retention_bytes";
1992
- asset_bytes: "asset_bytes";
1993
1993
  robot_online_ms: "robot_online_ms";
1994
1994
  }>;
1995
1995
  day: z.ZodString;
package/dist/routes.d.ts CHANGED
@@ -36,7 +36,7 @@ export type RouteAudience = 'developer' | 'client' | 'internal';
36
36
  */
37
37
  export type RouteAuth = 'developer' | 'developer_or_client' | 'none' | 'robot_upload' | 'in_handler' | 'ops';
38
38
  export type RouteTransport = 'http' | 'websocket';
39
- export type RouteSection = 'health' | 'developer-auth' | 'client-auth' | 'org' | 'users' | 'apps' | 'robots' | 'config' | 'alerts' | 'commands' | 'cameras' | 'assets' | 'mcp' | 'transports';
39
+ export type RouteSection = 'health' | 'developer-auth' | 'client-auth' | 'org' | 'billing' | 'users' | 'apps' | 'robots' | 'config' | 'alerts' | 'commands' | 'cameras' | 'assets' | 'mcp' | 'transports';
40
40
  export interface RouteParam {
41
41
  readonly name: string;
42
42
  /** One sentence: what the segment identifies and where a caller gets it. */
package/dist/routes.js CHANGED
@@ -12,12 +12,14 @@ import { jobRunListResponse, jobRunQuery, jobRunSummary, jobRunSummaryQuery } fr
12
12
  import { MCP_APP_PATHS, mcpRobotDatasheet, mcpRolePreviewResponse } from './mcp.js';
13
13
  import { authorizationServerMetadata, dynamicClientRegistrationRequest, dynamicClientRegistrationResponse, oauthAuthorizeQuery, oauthRedirectResponse, oauthTokenRequest, oauthTokenResponse, protectedResourceMetadata, } from './oauth.js';
14
14
  import { adminPlanChangeRequest, orgPlan, planChangeRequest } from './plans.js';
15
+ import { billingCancelRequest, billingChangeRequest, billingChangeResponse, billingDetailsUpdate, billingView, checkoutRequest, checkoutResponse, checkoutStatus, paymentMethodChangeRequest, vatIdCheckRequest, vatIdCheckResponse, } from './billing.js';
15
16
  import { cameraListResponse, cancelRequest, configDraftResponse, configVersionResponse, configVersionsResponse, createRobotRequest, createRobotResponse, datapointListResponse, datapointValue, exposureListResponse, fetchTypesRequest, fetchTypesResponse, historyQuery, historyResponse, introspectionResponse, invokeOrServiceResponse, invokeRequest, jobResponse, liveSessionResponse, orgHealthQuery, orgLatencyQuery, orgLatencyResponse, orgQuotaUsage, orgUsageQuery, orgUsageResponse, patchRobotRequest, patchRobotResponse, publishConfigResponse, publishRequest, putConfigDraftRequest, putRobotDetailsRequest, putRobotDetailsResponse, robotTokenRotateResponse, jointStatePutRequest, jointStatePutResponse, releaseLiveQuery, renameSlugRequest, renameSlugResponse, robotDeleteQuery, resourceHealthListResponse, robotDeletionSummary, robotDetailResponse, robotJobsResponse, robotListResponse, slugUsageResponse, snapshotMetaResponse, typesResponse, } from './rest.js';
16
17
  export const ROUTE_SECTIONS = [
17
18
  { id: 'health', title: 'Health' },
18
19
  { id: 'developer-auth', title: 'Developer auth' },
19
20
  { id: 'client-auth', title: 'App-user (client) auth' },
20
21
  { id: 'org', title: 'Org' },
22
+ { id: 'billing', title: 'Billing' },
21
23
  { id: 'users', title: 'Team' },
22
24
  { id: 'apps', title: 'Apps' },
23
25
  { id: 'robots', title: 'Robots' },
@@ -92,6 +94,16 @@ export const IN_HANDLER_ROUTES = [
92
94
  * than an undocumented one: a consumer branches on it and the branch is dead.
93
95
  */
94
96
  const DEVELOPER_GUARD = ['unauthorized', 'token_expired', 'token_revoked'];
97
+ /**
98
+ * The billing section's shorthand for the two refusals that come from
99
+ * billing's own infrastructure rather than from what the caller sent
100
+ * (2026-10-04, fleetless/fleetless#104): `BILLING_OFF` is no payment
101
+ * provider configured at all, `MOLLIE` is Mollie itself not
102
+ * answering. Every mutating billing route that talks to Mollie
103
+ * lists both; a route that only reads or edits local state lists neither.
104
+ */
105
+ const BILLING_OFF = ['billing_unavailable'];
106
+ const MOLLIE = ['payment_provider_unavailable'];
95
107
  /**
96
108
  * The same, for `auth: 'developer_or_client'` — one guard resolving a developer
97
109
  * bearer, an end-user bearer **or** a server key through a single token lookup.
@@ -2807,8 +2819,8 @@ export const ROUTES = [
2807
2819
  params: [], query: null, request: planChangeRequest, response: orgPlan,
2808
2820
  errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'plan_limit', 'target_state_conflict'], transport: 'http',
2809
2821
  notes: 'Owner tier, and **downward only**: this route moves the org to a lower plan or cancels it outright to Basic. It never moves the org ' +
2810
- 'up — until payment exists, an upgrade or an add-on is not this route\'s job at all, and is handled today as a Feedback request that ' +
2811
- 'Fleetless then applies through the admin route. `409 target_state_conflict` names `target_plan` with rule `not_lower` when the ' +
2822
+ 'up — an upgrade or an add-on goes through the billing routes instead (`POST /api/billing/checkout`, `POST /api/billing/change`). ' +
2823
+ '`409 target_state_conflict` names `target_plan` with rule `not_lower` when the ' +
2812
2824
  'chosen plan is not below the org\'s current one; with rule `locked_basic_only` when the org is locked (`orgLock`) and the chosen ' +
2813
2825
  'plan is anything but Basic; and with rule `migration_basic_only` when the org is still on the beta, awaiting the switch to priced ' +
2814
2826
  'plans, and the chosen plan is anything but Basic — that choice is exactly what the org lands on at the switch. An owner is never ' +
@@ -2837,14 +2849,179 @@ export const ROUTES = [
2837
2849
  audience: 'internal', auth: 'ops', rateLimited: true, ownerTier: false, status: 200,
2838
2850
  params: [{ name: 'id', description: 'The org\'s uuid, whose plan, add-ons or overrides the operator is changing.' }],
2839
2851
  query: null, request: adminPlanChangeRequest, response: orgPlan,
2840
- errors: ['unauthorized', 'not_found', 'validation_error', 'plan_limit', 'rate_limited'], transport: 'http',
2852
+ errors: ['unauthorized', 'not_found', 'validation_error', 'plan_limit', 'rate_limited', 'target_state_conflict'], transport: 'http',
2841
2853
  notes: 'Every public host answers `404 not_found` for every `/api/admin/*` path — this route is reachable only on the cloud\'s private ' +
2842
2854
  'address — and that same address answers `404` here too while `OPS_API_TOKEN` is not configured, so a door with no key behind it ' +
2843
2855
  'reads exactly like one nobody opened. An unknown `:id` is the same `404`. Applies at once, never queued as a `pending_change`, ' +
2844
2856
  'and only when the org\'s current usage fits the result: `409 plan_limit` when a lower plan, a lowered override or a removed add-on ' +
2845
2857
  'would leave the org over a limit — the operator never deletes an org\'s things, only the owner\'s own choice does. On success it ' +
2846
2858
  'also withdraws any `pending_change`, ends a beta org\'s wait for the switch and lifts a lock. Audited as `org.plan_changed` with ' +
2847
- 'the `fleetless` actor, never a developer\'s — the row names what an operator did, not who in the org asked for it.',
2859
+ 'the `fleetless` actor, never a developer\'s — the row names what an operator did, not who in the org asked for it. ' +
2860
+ '`409 target_state_conflict` names `currency` or `period_ends_at` with rule `billed` when the org has a billing account ' +
2861
+ 'in `active` or `past_due` and the request names either field: both are fixed at the first payment and billing, not the operator, ' +
2862
+ 'owns them from then on. The plan itself keeps working on a billed org — a plan, an add-on or an override the operator sets here is ' +
2863
+ 'charged from the next renewal, except `enterprise` and `basic`, which the sweep cancels the billing account for instead.',
2864
+ },
2865
+ /* --- billing (2026-10-04, fleetless/fleetless#104) ----------------------
2866
+ *
2867
+ * Every route here but the webhook is owner-only (`ownerTier: true`), a
2868
+ * developer answers `403 tier_required` and `GET /api/billing`
2869
+ * is no exception — a developer reads #103's plan cards from
2870
+ * `GET /api/org/plan` instead, without the payer, payment method or
2871
+ * invoices (Offene Punkte 5). `billing_unavailable` and
2872
+ * `payment_provider_unavailable` are declared in full on the two routes
2873
+ * above as `BILLING_OFF` and `MOLLIE`.
2874
+ */
2875
+ {
2876
+ method: 'GET', path: '/api/billing', section: 'billing',
2877
+ summary: "Reads the org's billing account, payment method and invoices.",
2878
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2879
+ params: [], query: null, request: null, response: billingView,
2880
+ errors: [...DEVELOPER_GUARD, 'tier_required'], transport: 'http',
2881
+ notes: '`available: false` when `MOLLIE_API_KEY` is not configured — this cloud takes no payments, and every mutating route on ' +
2882
+ 'this page answers `503 billing_unavailable` instead of acting. `account` is `null` before the org has ever checked out; the plan ' +
2883
+ 'and its limits still come from `GET /api/org/plan` (#103) and are not repeated here.',
2884
+ },
2885
+ {
2886
+ method: 'POST', path: '/api/billing/checkout', section: 'billing',
2887
+ summary: 'Starts a Mollie checkout for a plan, or an upgrade paid at once.',
2888
+ audience: 'developer', auth: 'developer', rateLimited: true, ownerTier: true, status: 201,
2889
+ params: [], query: null, request: checkoutRequest, response: checkoutResponse,
2890
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'plan_limit', 'target_state_conflict', 'rate_limited', ...BILLING_OFF, ...MOLLIE],
2891
+ transport: 'http',
2892
+ notes: 'Rate limited on the `billing.checkout` bucket, same as `POST /api/billing/payment-method` and `POST /api/billing/invoices/:id/pay` ' +
2893
+ '— the three routes that mint a Mollie checkout. `400 validation_error` names who may not pay with these rules: ' +
2894
+ '`{ field: \'billing.address.country\', rule: \'eu_person\' }` for a person in another EU country, ' +
2895
+ '`{ field: \'billing.vat_id\', rule: \'vat_id_required\' }` for a company there with no VAT ID, ' +
2896
+ '`{ field: \'billing.vat_id\', rule: \'vat_id_invalid\' }` once VIES has said so, and ' +
2897
+ '`{ field: \'accept_withdrawal\', rule: \'required\' }` for a person who did not confirm it. `409 target_state_conflict` names ' +
2898
+ '`plan` with rule `already_billed` when the org already has an `active` or `past_due` billing account — checkout is for the first ' +
2899
+ 'payment only, every later change is `POST /api/billing/change`. `checkout_url` is Mollie\'s hosted page; the return lands on ' +
2900
+ '`<console>/settings/billing?checkout=<checkout_id>`, which polls `GET /api/billing/checkout/:id` until the webhook — or the poll ' +
2901
+ 'itself — has reconciled the payment. `409 plan_limit` is the org\'s own usage against the plan being bought.',
2902
+ },
2903
+ {
2904
+ method: 'GET', path: '/api/billing/checkout/:id', section: 'billing',
2905
+ summary: "Reads a checkout's status, for the return page's poll.",
2906
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2907
+ params: [{ name: 'id', description: 'The checkout id from `checkoutResponse.checkout_id`, carried on the return URL.' }],
2908
+ query: null, request: null, response: checkoutStatus,
2909
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'not_found'], transport: 'http',
2910
+ notes: 'Calls the same `reconcilePayment(deps, molliePaymentId)` the webhook calls, so a return page that lands before the ' +
2911
+ 'webhook does still sees the payment applied — this route, not the webhook, is what the dev stack and the test-mode suite rely on, ' +
2912
+ 'since Mollie refuses an unreachable webhook URL. `plan` is the org\'s plan after applying, unchanged unless `purpose` ' +
2913
+ 'is `upgrade` and `status` is `paid`.',
2914
+ },
2915
+ {
2916
+ method: 'POST', path: '/api/billing/vat-id/check', section: 'billing',
2917
+ summary: 'Checks a VAT ID against VIES, for the checkout form and the details page.',
2918
+ audience: 'developer', auth: 'developer', rateLimited: true, ownerTier: true, status: 200,
2919
+ params: [], query: null, request: vatIdCheckRequest, response: vatIdCheckResponse,
2920
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'rate_limited'], transport: 'http',
2921
+ notes: 'Rate limited on its own `billing.vat_check` bucket, sized for a form checked on blur rather than for a checkout. Never refuses for ' +
2922
+ 'an `unverified` VIES answer — the checkout itself accepts `unverified` and the hourly sweep re-checks it — this route ' +
2923
+ 'only reports what VIES currently says, in a different place for the same ID, entered either at checkout or on `PATCH ' +
2924
+ '/api/billing/details`.',
2925
+ },
2926
+ {
2927
+ method: 'PATCH', path: '/api/billing/details', section: 'billing',
2928
+ summary: "Edits the billing account's invoice email or VAT ID.",
2929
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2930
+ params: [], query: null, request: billingDetailsUpdate, response: billingView,
2931
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'not_found'], transport: 'http',
2932
+ notes: '`404 not_found` when the org has no billing account yet — there is nothing here to edit before the first checkout. A new `vat_id` ' +
2933
+ 'is re-checked through VIES the same way `POST /api/billing/vat-id/check` does; a valid ID entered here lifts the block a definitive ' +
2934
+ '`invalid` answer placed on the next renewal.',
2935
+ },
2936
+ {
2937
+ method: 'POST', path: '/api/billing/change', section: 'billing',
2938
+ summary: "Moves the org's plan, cycle or add-ons, charging increases at once.",
2939
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2940
+ params: [], query: null, request: billingChangeRequest, response: billingChangeResponse,
2941
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'plan_limit', 'target_state_conflict', ...BILLING_OFF, ...MOLLIE],
2942
+ transport: 'http',
2943
+ notes: 'The body names the **absolute** target — plan, cycle and add-ons — never a delta: the route compares it with the ' +
2944
+ 'org\'s current state and splits the difference. The increasing part is charged now, through the same proration `changeNetCents` ' +
2945
+ 'computes, and takes effect the moment Mollie accepts the `recurring` payment with anything but `failed`, `canceled` or `expired` ' +
2946
+ '— cards answer within seconds, SEPA stays `pending` for days; if Mollie refuses or does not answer, nothing is applied ' +
2947
+ 'and this answers `502 payment_provider_unavailable` instead. The decreasing part — a lower plan, yearly → monthly, fewer add-ons — ' +
2948
+ 'is stored as a pending change and applied at the period\'s end, same as `PUT /api/org/plan/change`; a lower plan over the target\'s ' +
2949
+ 'limits answers `409 plan_limit` and the console opens #103\'s choose-what-stays page. Any increase first withdraws a pending ' +
2950
+ 'downgrade or cancel, exactly as the admin route does. `409 target_state_conflict` names `billing` with rule `no_account` (no ' +
2951
+ 'checkout yet — use `POST /api/billing/checkout`), `past_due` (an open invoice has to be paid first) or `no_valid_mandate` (the ' +
2952
+ 'payment method needs renewing first, `POST /api/billing/payment-method`). `charged` in the response is the invoice from the part ' +
2953
+ 'charged now, or `null` when the whole request was a decrease.',
2954
+ },
2955
+ {
2956
+ method: 'POST', path: '/api/billing/payment-method', section: 'billing',
2957
+ summary: 'Starts a Mollie checkout for a new payment method.',
2958
+ audience: 'developer', auth: 'developer', rateLimited: true, ownerTier: true, status: 201,
2959
+ params: [], query: null, request: paymentMethodChangeRequest, response: checkoutResponse,
2960
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'not_found', 'rate_limited', ...BILLING_OFF, ...MOLLIE],
2961
+ transport: 'http',
2962
+ notes: 'Rate limited on the `billing.checkout` bucket, same as `POST /api/billing/checkout`. Creates a `first` payment on the existing ' +
2963
+ 'Mollie customer: with an open invoice, the new payment **is** that invoice\'s amount, for any method the currency ' +
2964
+ 'allows, and pays it; without one, `card` and `paypal` use a zero amount and `sepa` is refused — Mollie allows a zero-amount first ' +
2965
+ 'payment for card and PayPal only — `400 validation_error` with `{ field: \'method\', rule: \'sepa_needs_open_invoice\' }`. SEPA is ' +
2966
+ 'also refused outside the org\'s EUR currency, `{ field: \'method\', rule: \'sepa_eur_only\' }`. `404 not_found` when the org has no ' +
2967
+ 'billing account yet. When the new mandate turns `valid`, every other mandate of the customer is revoked.',
2968
+ },
2969
+ {
2970
+ method: 'POST', path: '/api/billing/cancel', section: 'billing',
2971
+ summary: "Schedules the org's plan to cancel to Basic at the period's end.",
2972
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2973
+ params: [], query: null, request: billingCancelRequest, requestOptional: true, response: billingView,
2974
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'validation_error', 'not_found', 'plan_limit'], transport: 'http',
2975
+ notes: 'A body is optional — `reason` alone, and nobody but Fleetless reads it. `404 not_found` when the org has no billing account to ' +
2976
+ 'cancel. Takes effect at the period\'s end, nothing credited or refunded; over Basic\'s limits this is refused `409 ' +
2977
+ 'plan_limit` and the console sends the owner to #103\'s choose-what-stays page instead, the same as a plan downgrade. "Cancel at ' +
2978
+ 'period end" is not a flag here — it reads as `pending_change.target_plan === \'basic\'` on `GET /api/org/plan`.',
2979
+ },
2980
+ {
2981
+ method: 'POST', path: '/api/billing/resume', section: 'billing',
2982
+ summary: 'Withdraws a scheduled cancel, keeping the current plan.',
2983
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2984
+ params: [], query: null, request: null, response: billingView,
2985
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'not_found'], transport: 'http',
2986
+ notes: '`404 not_found` when nothing is pending — there is no scheduled cancel to withdraw. The org stays on its current plan, unchanged.',
2987
+ },
2988
+ {
2989
+ method: 'GET', path: '/api/billing/invoices/:id/pdf', section: 'billing',
2990
+ summary: "Downloads one invoice's rendered PDF.",
2991
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 200,
2992
+ params: [{ name: 'id', description: 'The invoice id, from `billingInvoice.id` in `GET /api/billing`\'s `invoices`.' }],
2993
+ query: null, request: null, response: null, contentType: 'application/pdf',
2994
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'not_found'], transport: 'http',
2995
+ notes: 'Rendered once, with `pdfkit`, when the invoice is issued, and stored as bytes — an issued invoice never changes, so this always ' +
2996
+ 'answers the same PDF for the same id. `404 not_found` for an unknown id or one from another org.',
2997
+ },
2998
+ {
2999
+ method: 'POST', path: '/api/billing/invoices/:id/pay', section: 'billing',
3000
+ summary: 'Pays one open invoice, through a new Mollie payment.',
3001
+ audience: 'developer', auth: 'developer', rateLimited: true, ownerTier: true, status: 201,
3002
+ params: [{ name: 'id', description: 'The invoice id, from `billingInvoice.id`, of the open invoice to pay.' }],
3003
+ query: null, request: null, response: checkoutResponse,
3004
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'not_found', 'target_state_conflict', 'rate_limited', ...BILLING_OFF, ...MOLLIE],
3005
+ transport: 'http',
3006
+ notes: 'Rate limited on the `billing.checkout` bucket, same as `POST /api/billing/checkout`. `409 target_state_conflict` names `invoice` ' +
3007
+ 'with rule `not_open` when the invoice is already `paid` or `uncollectible` — there is nothing left to pay. Paying the open ' +
3008
+ 'invoice of a locked org unlocks it and keeps the plan running to the period\'s end, the same as a renewal that ' +
3009
+ 'succeeds on a retry. `404 not_found` for an unknown id or one from another org.',
3010
+ },
3011
+ {
3012
+ method: 'POST', path: '/api/billing/mollie/webhook', section: 'billing',
3013
+ summary: "Takes Mollie's payment-changed notification and reconciles the payment.",
3014
+ audience: 'internal', auth: 'none', rateLimited: true, ownerTier: false, status: 200,
3015
+ params: [], query: null, request: null, response: null,
3016
+ errors: [], transport: 'http',
3017
+ notes: '**The body is never trusted**: it names only a payment id (`id=tr_…`, form-encoded, Mollie\'s own shape), and this route ' +
3018
+ 'does nothing with it but call `reconcilePayment(deps, molliePaymentId)` — the same function `GET /api/billing/checkout/:id` and ' +
3019
+ 'the hourly sweep call — which fetches the payment from Mollie itself and applies what Mollie says, idempotently. Rate limited on ' +
3020
+ 'the `billing.webhook` bucket, per ip, 600/min — generous, because this is Mollie\'s own infrastructure calling, not a browser. ' +
3021
+ '**Every answer is `200`**, including an id this cloud does not recognise, which is logged and otherwise ignored, **except a ' +
3022
+ 'processing fault, which is `500`** so Mollie retries the notification rather than this cloud losing it. `auth: \'none\'` because ' +
3023
+ 'Mollie signs nothing Fleetless checks here — the payment is only ever trusted once fetched back from Mollie\'s own API with the ' +
3024
+ 'configured key.',
2848
3025
  },
2849
3026
  /* ------------------------------------------------- assets (robot upload) */
2850
3027
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "6.2.0",
3
+ "version": "6.3.0-next.1",
4
4
  "description": "Fleetless wire contracts: the bridge-cloud protocol, the REST API schemas and the error codes, as zod schemas with generated JSON Schema and OpenAPI artifacts.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dehne Robotik GmbH",