@munchi_oy/license-sdk 0.8.0 → 0.10.0

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @munchi_oy/license-sdk
2
2
 
3
- Asks the Munchi license service what a venue may use. Zero dependencies, Node 20+, ESM and CommonJS.
3
+ Asks the Munchi license service what a venue may use. One dependency (`zod`, for the event schema), Node 20+, ESM and CommonJS.
4
4
 
5
5
  ```ts
6
6
  import { LicenseClient } from "@munchi_oy/license-sdk";
@@ -31,6 +31,8 @@ const snapshot = await license.entitlements(venueId); // full snapshot, null = u
31
31
 
32
32
  Call `invalidate(venueId)` when you receive a `license.changed` event.
33
33
 
34
+ A service that runs a store's channels subscribes to `license.access_changed` (topic `one-license-<env>-access`) and validates each message with the exported `accessChangedEventSchema` (type `AccessChangedEvent`, kind `ACCESS_CHANGED`); drop an `id` already applied and a `snapshotVersion` older than the last one applied for the venue. `hasAccess(event)` is false when the store may use nothing (`suspended`, `terminated`, `canceled`: an unpaid store too), the same rule as `can()`; a feature is usable when it is true and the feature is in `features`.
35
+
34
36
  It also exports the shared names: `PRODUCTS` and `PLANS` (catalog codes), `USAGE_METRICS` (what usage can cost money) and `REQUESTABLE_PRODUCTS` (what a store asks Munchi for in the dashboard: `ari`, `business_account`, `capital`, `foodora`, `bolt`, `card_payments_pos`, `card_payments_kiosk`, `sevenrooms`, `tableonline`, `zenchef`), with `STORE_ONLY_PRODUCTS` and `isStoreOnlyProduct` for the ones only a store asks for, never the whole group (the delivery apps, card payments at the POS or kiosk, and the reservation systems).
35
37
 
36
- For a group's company (ADR 0025) it exports `companyIds(country, { businessId, vatId })` (the IDs to store by the company's country, or `{ invalid: "businessId" | "vatId" }`; any country is accepted, Finland's are checked), `finnishBusinessId(input)` (the Y-tunnus as `NNNNNNN-C`, or `null` when its format or check digit is wrong), `finnishVatId(businessId)` (`FI` and its digits), and the types `LegalEntityChange` (the body of `PATCH /v1/accounts/:accountId/legal-entities/:legalEntityId`) and `LegalEntity` (its response).
38
+ For a group's company (ADR 0025) it exports `companyIds(country, { businessId, vatId })` (the IDs to store, as written and trimmed; nothing is refused, a well-formed Finnish Y-tunnus is written `NNNNNNN-C` and its VAT ID derived when none is given), `finnishBusinessId(input)` (the Y-tunnus as `NNNNNNN-C`, or `null` when its format or check digit is wrong), `finnishVatId(businessId)` (`FI` and its digits), and the types `LegalEntityChange` (the body of `PATCH /v1/accounts/:accountId/legal-entities/:legalEntityId`) and `LegalEntity` (its response).
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ ACCESS_CHANGED: () => ACCESS_CHANGED,
24
25
  LicenseClient: () => LicenseClient,
25
26
  PLANS: () => PLANS,
26
27
  PRODUCTS: () => PRODUCTS,
@@ -28,9 +29,11 @@ __export(index_exports, {
28
29
  STORE_ONLY_PRODUCTS: () => STORE_ONLY_PRODUCTS,
29
30
  USAGE_METRICS: () => USAGE_METRICS,
30
31
  USAGE_METRIC_NAMES: () => USAGE_METRIC_NAMES,
32
+ accessChangedEventSchema: () => accessChangedEventSchema,
31
33
  companyIds: () => companyIds,
32
34
  finnishBusinessId: () => finnishBusinessId,
33
35
  finnishVatId: () => finnishVatId,
36
+ hasAccess: () => hasAccess,
34
37
  isStoreOnlyProduct: () => isStoreOnlyProduct
35
38
  });
36
39
  module.exports = __toCommonJS(index_exports);
@@ -41,6 +44,9 @@ var PRODUCTS = {
41
44
  one: "MUN-ONE",
42
45
  kiosk: "MUN-ADD-KIOSK",
43
46
  mobileApp: "MUN-ADD-APP",
47
+ munchiOrder: "MUN-ADD-ORDER",
48
+ paymentTerminal: "MUN-ADD-TERMINAL",
49
+ hardware: "MUN-ADD-HARDWARE",
44
50
  mobileAppSetup: "MUN-SETUP-APP",
45
51
  sms: "MUN-SMS",
46
52
  loyaltyOverage: "MUN-OVR-LOYALTY"
@@ -50,6 +56,49 @@ var PLANS = [
50
56
  PRODUCTS.one
51
57
  ];
52
58
 
59
+ // ../license-core/src/events/access-changed.ts
60
+ var import_zod = require("zod");
61
+ var ACCESS_CHANGED = "license.access_changed";
62
+ var NO_ACCESS_STATES = /* @__PURE__ */ new Set([
63
+ "suspended",
64
+ "terminated",
65
+ "canceled"
66
+ ]);
67
+ var accessChangedEventSchema = import_zod.z.object({
68
+ kind: import_zod.z.literal(ACCESS_CHANGED),
69
+ /** The event's own id: a redelivered message has the same one. */
70
+ id: import_zod.z.string().min(1),
71
+ venueId: import_zod.z.number().int(),
72
+ locationId: import_zod.z.string().min(1),
73
+ accountId: import_zod.z.string().min(1),
74
+ state: import_zod.z.enum([
75
+ "trial",
76
+ "active",
77
+ "grace",
78
+ "start",
79
+ "suspended",
80
+ "terminated",
81
+ "canceled"
82
+ ]),
83
+ /** As in the snapshot: empty when the store is closed or switched off. */
84
+ features: import_zod.z.array(import_zod.z.string().min(1)),
85
+ /** As in the snapshot: how many of something the store may have; `null` = unlimited. */
86
+ limits: import_zod.z.record(import_zod.z.string(), import_zod.z.number().int().min(0).nullable()),
87
+ suspension: import_zod.z.object({
88
+ reason: import_zod.z.string().min(1),
89
+ mode: import_zod.z.enum([
90
+ "read_only",
91
+ "off"
92
+ ])
93
+ }).nullable(),
94
+ /** The audit action that changed it, e.g. `license.close` or `license.payment.suspend`. */
95
+ reason: import_zod.z.string().min(1),
96
+ /** The snapshot's `v`: a consumer drops a message older than the last one it applied. */
97
+ snapshotVersion: import_zod.z.number().int().min(0),
98
+ at: import_zod.z.iso.datetime()
99
+ });
100
+ var hasAccess = /* @__PURE__ */ __name((event) => !NO_ACCESS_STATES.has(event.state), "hasAccess");
101
+
53
102
  // ../license-core/src/legal-entity.ts
54
103
  var Y_TUNNUS = /^(\d{6,7})-(\d)$/;
55
104
  var Y_TUNNUS_WEIGHTS = [
@@ -70,36 +119,24 @@ var finnishBusinessId = /* @__PURE__ */ __name((input) => {
70
119
  return Number(check) === expected ? `${digits}-${check}` : null;
71
120
  }, "finnishBusinessId");
72
121
  var finnishVatId = /* @__PURE__ */ __name((businessId) => `FI${businessId.replace("-", "")}`, "finnishVatId");
73
- var ANY_COUNTRY = {
74
- businessId: /* @__PURE__ */ __name((input) => input.trim() || null, "businessId"),
75
- vatId: /* @__PURE__ */ __name((_businessId, input) => input?.trim() || null, "vatId")
76
- };
77
- var COMPANY_IDS_BY_COUNTRY = {
78
- FI: {
79
- businessId: finnishBusinessId,
80
- vatId: /* @__PURE__ */ __name((businessId, input) => {
81
- const vatId = finnishVatId(businessId);
82
- return input === null || input.replace(/\s/g, "").toUpperCase() === vatId ? vatId : void 0;
83
- }, "vatId")
84
- }
85
- };
86
122
  var companyIds = /* @__PURE__ */ __name((country, input) => {
87
- const rules = COMPANY_IDS_BY_COUNTRY[country.toUpperCase()] ?? ANY_COUNTRY;
88
- const businessId = rules.businessId(input.businessId);
89
- if (businessId === null) return {
90
- invalid: "businessId"
91
- };
92
- const vatId = rules.vatId(businessId, input.vatId ?? null);
93
- if (vatId === void 0) return {
94
- invalid: "vatId"
95
- };
96
- return {
123
+ const businessId = input.businessId.trim();
124
+ const vatId = input.vatId?.trim() || null;
125
+ const finnish = country.toUpperCase() === "FI" ? finnishBusinessId(businessId) : null;
126
+ if (finnish === null) return {
97
127
  businessId,
98
128
  vatId
99
129
  };
130
+ const derived = finnishVatId(finnish);
131
+ const matches = vatId === null || vatId.replace(/\s/g, "").toUpperCase() === derived;
132
+ return {
133
+ businessId: finnish,
134
+ vatId: matches ? derived : vatId
135
+ };
100
136
  }, "companyIds");
101
137
 
102
138
  // ../license-core/src/requestable-products.ts
139
+ var import_zod2 = require("zod");
103
140
  var REQUESTABLE_PRODUCTS = [
104
141
  "ari",
105
142
  "business_account",
@@ -110,7 +147,9 @@ var REQUESTABLE_PRODUCTS = [
110
147
  "card_payments_kiosk",
111
148
  "sevenrooms",
112
149
  "tableonline",
113
- "zenchef"
150
+ "zenchef",
151
+ "payment_terminal",
152
+ "hardware"
114
153
  ];
115
154
  var STORE_ONLY_PRODUCTS = [
116
155
  "foodora",
@@ -119,10 +158,18 @@ var STORE_ONLY_PRODUCTS = [
119
158
  "card_payments_kiosk",
120
159
  "sevenrooms",
121
160
  "tableonline",
122
- "zenchef"
161
+ "zenchef",
162
+ "payment_terminal",
163
+ "hardware"
123
164
  ];
124
165
  var storeOnly = new Set(STORE_ONLY_PRODUCTS);
125
166
  var isStoreOnlyProduct = /* @__PURE__ */ __name((product) => storeOnly.has(product), "isStoreOnlyProduct");
167
+ var MAX_DETAILS_BYTES = 1e4;
168
+ var productRequestDetails = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.json()).refine((details) => Buffer.byteLength(JSON.stringify(details)) <= MAX_DETAILS_BYTES, {
169
+ message: `details may be at most ${MAX_DETAILS_BYTES} bytes`
170
+ }).refine((details) => !JSON.stringify(details).includes("\\u0000"), {
171
+ message: "details may not contain the null character"
172
+ });
126
173
 
127
174
  // ../license-core/src/usage-metrics.ts
128
175
  var USAGE_METRICS = {
@@ -271,6 +318,7 @@ var LicenseClient = class {
271
318
  };
272
319
  // Annotate the CommonJS export names for ESM import in node:
273
320
  0 && (module.exports = {
321
+ ACCESS_CHANGED,
274
322
  LicenseClient,
275
323
  PLANS,
276
324
  PRODUCTS,
@@ -278,8 +326,10 @@ var LicenseClient = class {
278
326
  STORE_ONLY_PRODUCTS,
279
327
  USAGE_METRICS,
280
328
  USAGE_METRIC_NAMES,
329
+ accessChangedEventSchema,
281
330
  companyIds,
282
331
  finnishBusinessId,
283
332
  finnishVatId,
333
+ hasAccess,
284
334
  isStoreOnlyProduct
285
335
  });
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * Munchi's product codes, for code that names one (apps, the dashboard). Codes are immutable and
3
5
  * never reused; a catalog may still carry products added later, so this is not the full list.
@@ -7,6 +9,9 @@ declare const PRODUCTS: {
7
9
  readonly one: "MUN-ONE";
8
10
  readonly kiosk: "MUN-ADD-KIOSK";
9
11
  readonly mobileApp: "MUN-ADD-APP";
12
+ readonly munchiOrder: "MUN-ADD-ORDER";
13
+ readonly paymentTerminal: "MUN-ADD-TERMINAL";
14
+ readonly hardware: "MUN-ADD-HARDWARE";
10
15
  readonly mobileAppSetup: "MUN-SETUP-APP";
11
16
  readonly sms: "MUN-SMS";
12
17
  readonly loyaltyOverage: "MUN-OVR-LOYALTY";
@@ -16,6 +21,47 @@ type KnownProductCode = (typeof PRODUCTS)[keyof typeof PRODUCTS];
16
21
  declare const PLANS: readonly ["MUN-START", "MUN-ONE"];
17
22
  type PlanCode = (typeof PLANS)[number];
18
23
 
24
+ declare const ACCESS_CHANGED = "license.access_changed";
25
+ /**
26
+ * What a store may use, sent on `ACCESS_TOPIC` whenever it changes (closed, suspended, unpaid,
27
+ * paid again, an add-on bought or removed). Only zod, because the SDK ships it to consumers.
28
+ * Unknown fields are ignored, so fields added later don't break an older consumer.
29
+ */
30
+ declare const accessChangedEventSchema: z.ZodObject<{
31
+ kind: z.ZodLiteral<"license.access_changed">;
32
+ id: z.ZodString;
33
+ venueId: z.ZodNumber;
34
+ locationId: z.ZodString;
35
+ accountId: z.ZodString;
36
+ state: z.ZodEnum<{
37
+ start: "start";
38
+ suspended: "suspended";
39
+ terminated: "terminated";
40
+ canceled: "canceled";
41
+ trial: "trial";
42
+ active: "active";
43
+ grace: "grace";
44
+ }>;
45
+ features: z.ZodArray<z.ZodString>;
46
+ limits: z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodNumber>>;
47
+ suspension: z.ZodNullable<z.ZodObject<{
48
+ reason: z.ZodString;
49
+ mode: z.ZodEnum<{
50
+ read_only: "read_only";
51
+ off: "off";
52
+ }>;
53
+ }, z.core.$strip>>;
54
+ reason: z.ZodString;
55
+ snapshotVersion: z.ZodNumber;
56
+ at: z.ZodISODateTime;
57
+ }, z.core.$strip>;
58
+ type AccessChangedEvent = z.infer<typeof accessChangedEventSchema>;
59
+ /**
60
+ * May the store use anything now? False when closed, suspended (an unpaid store too: everything
61
+ * off, owner 2026-11-04) or terminated. A feature is usable when this is true and it is in `features`.
62
+ */
63
+ declare const hasAccess: (event: Pick<AccessChangedEvent, "state">) => boolean;
64
+
19
65
  /**
20
66
  * A group's company (legal entity), as the dashboard reads and changes it. One company can be the
21
67
  * company of several stores of the same group. Zero-dependency on purpose: the SDK ships it.
@@ -51,14 +97,16 @@ interface LegalEntityChange {
51
97
  declare const finnishBusinessId: (input: string) => string | null;
52
98
  /** A Finnish VAT ID is always `FI` and the business ID's eight digits. */
53
99
  declare const finnishVatId: (businessId: string) => string;
54
- /** The IDs to store, or the field that is wrong for the company's country. */
55
- type CompanyIds = {
100
+ /** The IDs to store. Nothing is refused (owner, 2026-11-01: business IDs aren't validated). */
101
+ interface CompanyIds {
56
102
  businessId: string;
57
103
  vatId: string | null;
58
- } | {
59
- invalid: "businessId" | "vatId";
60
- };
61
- /** Checks and normalises a company's IDs by the rules of its country (ISO 3166-1 alpha-2). */
104
+ }
105
+ /**
106
+ * The company's IDs as written, trimmed. A Finnish company's well-formed Y-tunnus is written as
107
+ * `NNNNNNN-C`, and its VAT ID is derived when none is given or written like it in another case or
108
+ * spacing.
109
+ */
62
110
  declare const companyIds: (country: string, input: {
63
111
  businessId: string;
64
112
  vatId?: string | null;
@@ -69,13 +117,13 @@ declare const companyIds: (country: string, input: {
69
117
  * (ADR 0022). These are the dashboard's product ids, not catalog codes (`MUN-*`); the mobile app
70
118
  * has its own request flow (ADR 0021). A new one is added here first.
71
119
  */
72
- declare const REQUESTABLE_PRODUCTS: readonly ["ari", "business_account", "capital", "foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef"];
120
+ declare const REQUESTABLE_PRODUCTS: readonly ["ari", "business_account", "capital", "foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef", "payment_terminal", "hardware"];
73
121
  type RequestableProduct = (typeof REQUESTABLE_PRODUCTS)[number];
74
122
  /**
75
- * Delivery apps, card payments at the POS or kiosk and reservation systems are set up one store at
76
- * a time, so only a store asks for them, never the whole group.
123
+ * Delivery apps, card payments at the POS or kiosk, reservation systems and hardware are set up one
124
+ * store at a time, so only a store asks for them, never the whole group.
77
125
  */
78
- declare const STORE_ONLY_PRODUCTS: readonly ["foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef"];
126
+ declare const STORE_ONLY_PRODUCTS: readonly ["foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef", "payment_terminal", "hardware"];
79
127
  declare const isStoreOnlyProduct: (product: RequestableProduct) => boolean;
80
128
 
81
129
  /**
@@ -228,4 +276,4 @@ declare class LicenseClient {
228
276
  private report;
229
277
  }
230
278
 
231
- export { type CompanyIds, type DeniedEvent, type KnownProductCode, type LegalEntity, type LegalEntityChange, LicenseClient, type LicenseClientOptions, PLANS, PRODUCTS, type PlanCode, REQUESTABLE_PRODUCTS, type RequestableProduct, STORE_ONLY_PRODUCTS, type Snapshot, USAGE_METRICS, USAGE_METRIC_NAMES, type UsageMetric, companyIds, finnishBusinessId, finnishVatId, isStoreOnlyProduct };
279
+ export { ACCESS_CHANGED, type AccessChangedEvent, type CompanyIds, type DeniedEvent, type KnownProductCode, type LegalEntity, type LegalEntityChange, LicenseClient, type LicenseClientOptions, PLANS, PRODUCTS, type PlanCode, REQUESTABLE_PRODUCTS, type RequestableProduct, STORE_ONLY_PRODUCTS, type Snapshot, USAGE_METRICS, USAGE_METRIC_NAMES, type UsageMetric, accessChangedEventSchema, companyIds, finnishBusinessId, finnishVatId, hasAccess, isStoreOnlyProduct };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * Munchi's product codes, for code that names one (apps, the dashboard). Codes are immutable and
3
5
  * never reused; a catalog may still carry products added later, so this is not the full list.
@@ -7,6 +9,9 @@ declare const PRODUCTS: {
7
9
  readonly one: "MUN-ONE";
8
10
  readonly kiosk: "MUN-ADD-KIOSK";
9
11
  readonly mobileApp: "MUN-ADD-APP";
12
+ readonly munchiOrder: "MUN-ADD-ORDER";
13
+ readonly paymentTerminal: "MUN-ADD-TERMINAL";
14
+ readonly hardware: "MUN-ADD-HARDWARE";
10
15
  readonly mobileAppSetup: "MUN-SETUP-APP";
11
16
  readonly sms: "MUN-SMS";
12
17
  readonly loyaltyOverage: "MUN-OVR-LOYALTY";
@@ -16,6 +21,47 @@ type KnownProductCode = (typeof PRODUCTS)[keyof typeof PRODUCTS];
16
21
  declare const PLANS: readonly ["MUN-START", "MUN-ONE"];
17
22
  type PlanCode = (typeof PLANS)[number];
18
23
 
24
+ declare const ACCESS_CHANGED = "license.access_changed";
25
+ /**
26
+ * What a store may use, sent on `ACCESS_TOPIC` whenever it changes (closed, suspended, unpaid,
27
+ * paid again, an add-on bought or removed). Only zod, because the SDK ships it to consumers.
28
+ * Unknown fields are ignored, so fields added later don't break an older consumer.
29
+ */
30
+ declare const accessChangedEventSchema: z.ZodObject<{
31
+ kind: z.ZodLiteral<"license.access_changed">;
32
+ id: z.ZodString;
33
+ venueId: z.ZodNumber;
34
+ locationId: z.ZodString;
35
+ accountId: z.ZodString;
36
+ state: z.ZodEnum<{
37
+ start: "start";
38
+ suspended: "suspended";
39
+ terminated: "terminated";
40
+ canceled: "canceled";
41
+ trial: "trial";
42
+ active: "active";
43
+ grace: "grace";
44
+ }>;
45
+ features: z.ZodArray<z.ZodString>;
46
+ limits: z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodNumber>>;
47
+ suspension: z.ZodNullable<z.ZodObject<{
48
+ reason: z.ZodString;
49
+ mode: z.ZodEnum<{
50
+ read_only: "read_only";
51
+ off: "off";
52
+ }>;
53
+ }, z.core.$strip>>;
54
+ reason: z.ZodString;
55
+ snapshotVersion: z.ZodNumber;
56
+ at: z.ZodISODateTime;
57
+ }, z.core.$strip>;
58
+ type AccessChangedEvent = z.infer<typeof accessChangedEventSchema>;
59
+ /**
60
+ * May the store use anything now? False when closed, suspended (an unpaid store too: everything
61
+ * off, owner 2026-11-04) or terminated. A feature is usable when this is true and it is in `features`.
62
+ */
63
+ declare const hasAccess: (event: Pick<AccessChangedEvent, "state">) => boolean;
64
+
19
65
  /**
20
66
  * A group's company (legal entity), as the dashboard reads and changes it. One company can be the
21
67
  * company of several stores of the same group. Zero-dependency on purpose: the SDK ships it.
@@ -51,14 +97,16 @@ interface LegalEntityChange {
51
97
  declare const finnishBusinessId: (input: string) => string | null;
52
98
  /** A Finnish VAT ID is always `FI` and the business ID's eight digits. */
53
99
  declare const finnishVatId: (businessId: string) => string;
54
- /** The IDs to store, or the field that is wrong for the company's country. */
55
- type CompanyIds = {
100
+ /** The IDs to store. Nothing is refused (owner, 2026-11-01: business IDs aren't validated). */
101
+ interface CompanyIds {
56
102
  businessId: string;
57
103
  vatId: string | null;
58
- } | {
59
- invalid: "businessId" | "vatId";
60
- };
61
- /** Checks and normalises a company's IDs by the rules of its country (ISO 3166-1 alpha-2). */
104
+ }
105
+ /**
106
+ * The company's IDs as written, trimmed. A Finnish company's well-formed Y-tunnus is written as
107
+ * `NNNNNNN-C`, and its VAT ID is derived when none is given or written like it in another case or
108
+ * spacing.
109
+ */
62
110
  declare const companyIds: (country: string, input: {
63
111
  businessId: string;
64
112
  vatId?: string | null;
@@ -69,13 +117,13 @@ declare const companyIds: (country: string, input: {
69
117
  * (ADR 0022). These are the dashboard's product ids, not catalog codes (`MUN-*`); the mobile app
70
118
  * has its own request flow (ADR 0021). A new one is added here first.
71
119
  */
72
- declare const REQUESTABLE_PRODUCTS: readonly ["ari", "business_account", "capital", "foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef"];
120
+ declare const REQUESTABLE_PRODUCTS: readonly ["ari", "business_account", "capital", "foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef", "payment_terminal", "hardware"];
73
121
  type RequestableProduct = (typeof REQUESTABLE_PRODUCTS)[number];
74
122
  /**
75
- * Delivery apps, card payments at the POS or kiosk and reservation systems are set up one store at
76
- * a time, so only a store asks for them, never the whole group.
123
+ * Delivery apps, card payments at the POS or kiosk, reservation systems and hardware are set up one
124
+ * store at a time, so only a store asks for them, never the whole group.
77
125
  */
78
- declare const STORE_ONLY_PRODUCTS: readonly ["foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef"];
126
+ declare const STORE_ONLY_PRODUCTS: readonly ["foodora", "bolt", "card_payments_pos", "card_payments_kiosk", "sevenrooms", "tableonline", "zenchef", "payment_terminal", "hardware"];
79
127
  declare const isStoreOnlyProduct: (product: RequestableProduct) => boolean;
80
128
 
81
129
  /**
@@ -228,4 +276,4 @@ declare class LicenseClient {
228
276
  private report;
229
277
  }
230
278
 
231
- export { type CompanyIds, type DeniedEvent, type KnownProductCode, type LegalEntity, type LegalEntityChange, LicenseClient, type LicenseClientOptions, PLANS, PRODUCTS, type PlanCode, REQUESTABLE_PRODUCTS, type RequestableProduct, STORE_ONLY_PRODUCTS, type Snapshot, USAGE_METRICS, USAGE_METRIC_NAMES, type UsageMetric, companyIds, finnishBusinessId, finnishVatId, isStoreOnlyProduct };
279
+ export { ACCESS_CHANGED, type AccessChangedEvent, type CompanyIds, type DeniedEvent, type KnownProductCode, type LegalEntity, type LegalEntityChange, LicenseClient, type LicenseClientOptions, PLANS, PRODUCTS, type PlanCode, REQUESTABLE_PRODUCTS, type RequestableProduct, STORE_ONLY_PRODUCTS, type Snapshot, USAGE_METRICS, USAGE_METRIC_NAMES, type UsageMetric, accessChangedEventSchema, companyIds, finnishBusinessId, finnishVatId, hasAccess, isStoreOnlyProduct };
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ var PRODUCTS = {
7
7
  one: "MUN-ONE",
8
8
  kiosk: "MUN-ADD-KIOSK",
9
9
  mobileApp: "MUN-ADD-APP",
10
+ munchiOrder: "MUN-ADD-ORDER",
11
+ paymentTerminal: "MUN-ADD-TERMINAL",
12
+ hardware: "MUN-ADD-HARDWARE",
10
13
  mobileAppSetup: "MUN-SETUP-APP",
11
14
  sms: "MUN-SMS",
12
15
  loyaltyOverage: "MUN-OVR-LOYALTY"
@@ -16,6 +19,49 @@ var PLANS = [
16
19
  PRODUCTS.one
17
20
  ];
18
21
 
22
+ // ../license-core/src/events/access-changed.ts
23
+ import { z } from "zod";
24
+ var ACCESS_CHANGED = "license.access_changed";
25
+ var NO_ACCESS_STATES = /* @__PURE__ */ new Set([
26
+ "suspended",
27
+ "terminated",
28
+ "canceled"
29
+ ]);
30
+ var accessChangedEventSchema = z.object({
31
+ kind: z.literal(ACCESS_CHANGED),
32
+ /** The event's own id: a redelivered message has the same one. */
33
+ id: z.string().min(1),
34
+ venueId: z.number().int(),
35
+ locationId: z.string().min(1),
36
+ accountId: z.string().min(1),
37
+ state: z.enum([
38
+ "trial",
39
+ "active",
40
+ "grace",
41
+ "start",
42
+ "suspended",
43
+ "terminated",
44
+ "canceled"
45
+ ]),
46
+ /** As in the snapshot: empty when the store is closed or switched off. */
47
+ features: z.array(z.string().min(1)),
48
+ /** As in the snapshot: how many of something the store may have; `null` = unlimited. */
49
+ limits: z.record(z.string(), z.number().int().min(0).nullable()),
50
+ suspension: z.object({
51
+ reason: z.string().min(1),
52
+ mode: z.enum([
53
+ "read_only",
54
+ "off"
55
+ ])
56
+ }).nullable(),
57
+ /** The audit action that changed it, e.g. `license.close` or `license.payment.suspend`. */
58
+ reason: z.string().min(1),
59
+ /** The snapshot's `v`: a consumer drops a message older than the last one it applied. */
60
+ snapshotVersion: z.number().int().min(0),
61
+ at: z.iso.datetime()
62
+ });
63
+ var hasAccess = /* @__PURE__ */ __name((event) => !NO_ACCESS_STATES.has(event.state), "hasAccess");
64
+
19
65
  // ../license-core/src/legal-entity.ts
20
66
  var Y_TUNNUS = /^(\d{6,7})-(\d)$/;
21
67
  var Y_TUNNUS_WEIGHTS = [
@@ -36,36 +82,24 @@ var finnishBusinessId = /* @__PURE__ */ __name((input) => {
36
82
  return Number(check) === expected ? `${digits}-${check}` : null;
37
83
  }, "finnishBusinessId");
38
84
  var finnishVatId = /* @__PURE__ */ __name((businessId) => `FI${businessId.replace("-", "")}`, "finnishVatId");
39
- var ANY_COUNTRY = {
40
- businessId: /* @__PURE__ */ __name((input) => input.trim() || null, "businessId"),
41
- vatId: /* @__PURE__ */ __name((_businessId, input) => input?.trim() || null, "vatId")
42
- };
43
- var COMPANY_IDS_BY_COUNTRY = {
44
- FI: {
45
- businessId: finnishBusinessId,
46
- vatId: /* @__PURE__ */ __name((businessId, input) => {
47
- const vatId = finnishVatId(businessId);
48
- return input === null || input.replace(/\s/g, "").toUpperCase() === vatId ? vatId : void 0;
49
- }, "vatId")
50
- }
51
- };
52
85
  var companyIds = /* @__PURE__ */ __name((country, input) => {
53
- const rules = COMPANY_IDS_BY_COUNTRY[country.toUpperCase()] ?? ANY_COUNTRY;
54
- const businessId = rules.businessId(input.businessId);
55
- if (businessId === null) return {
56
- invalid: "businessId"
57
- };
58
- const vatId = rules.vatId(businessId, input.vatId ?? null);
59
- if (vatId === void 0) return {
60
- invalid: "vatId"
61
- };
62
- return {
86
+ const businessId = input.businessId.trim();
87
+ const vatId = input.vatId?.trim() || null;
88
+ const finnish = country.toUpperCase() === "FI" ? finnishBusinessId(businessId) : null;
89
+ if (finnish === null) return {
63
90
  businessId,
64
91
  vatId
65
92
  };
93
+ const derived = finnishVatId(finnish);
94
+ const matches = vatId === null || vatId.replace(/\s/g, "").toUpperCase() === derived;
95
+ return {
96
+ businessId: finnish,
97
+ vatId: matches ? derived : vatId
98
+ };
66
99
  }, "companyIds");
67
100
 
68
101
  // ../license-core/src/requestable-products.ts
102
+ import { z as z2 } from "zod";
69
103
  var REQUESTABLE_PRODUCTS = [
70
104
  "ari",
71
105
  "business_account",
@@ -76,7 +110,9 @@ var REQUESTABLE_PRODUCTS = [
76
110
  "card_payments_kiosk",
77
111
  "sevenrooms",
78
112
  "tableonline",
79
- "zenchef"
113
+ "zenchef",
114
+ "payment_terminal",
115
+ "hardware"
80
116
  ];
81
117
  var STORE_ONLY_PRODUCTS = [
82
118
  "foodora",
@@ -85,10 +121,18 @@ var STORE_ONLY_PRODUCTS = [
85
121
  "card_payments_kiosk",
86
122
  "sevenrooms",
87
123
  "tableonline",
88
- "zenchef"
124
+ "zenchef",
125
+ "payment_terminal",
126
+ "hardware"
89
127
  ];
90
128
  var storeOnly = new Set(STORE_ONLY_PRODUCTS);
91
129
  var isStoreOnlyProduct = /* @__PURE__ */ __name((product) => storeOnly.has(product), "isStoreOnlyProduct");
130
+ var MAX_DETAILS_BYTES = 1e4;
131
+ var productRequestDetails = z2.record(z2.string(), z2.json()).refine((details) => Buffer.byteLength(JSON.stringify(details)) <= MAX_DETAILS_BYTES, {
132
+ message: `details may be at most ${MAX_DETAILS_BYTES} bytes`
133
+ }).refine((details) => !JSON.stringify(details).includes("\\u0000"), {
134
+ message: "details may not contain the null character"
135
+ });
92
136
 
93
137
  // ../license-core/src/usage-metrics.ts
94
138
  var USAGE_METRICS = {
@@ -236,6 +280,7 @@ var LicenseClient = class {
236
280
  }
237
281
  };
238
282
  export {
283
+ ACCESS_CHANGED,
239
284
  LicenseClient,
240
285
  PLANS,
241
286
  PRODUCTS,
@@ -243,8 +288,10 @@ export {
243
288
  STORE_ONLY_PRODUCTS,
244
289
  USAGE_METRICS,
245
290
  USAGE_METRIC_NAMES,
291
+ accessChangedEventSchema,
246
292
  companyIds,
247
293
  finnishBusinessId,
248
294
  finnishVatId,
295
+ hasAccess,
249
296
  isStoreOnlyProduct
250
297
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@munchi_oy/license-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Client for the Munchi license service: cached entitlements and feature checks per venue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -33,6 +33,9 @@
33
33
  "url": "git+https://github.com/gomunchi/one-license.git",
34
34
  "directory": "packages/license-sdk"
35
35
  },
36
+ "dependencies": {
37
+ "zod": "4.6.7"
38
+ },
36
39
  "devDependencies": {
37
40
  "@munchi_oy/license-core": "0.1.0",
38
41
  "@swc/core": "1.16.12",