@endora-commerce/contracts 0.100.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/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/actor.d.ts +79 -0
- package/dist/actor.d.ts.map +1 -0
- package/dist/actor.js +41 -0
- package/dist/actor.js.map +1 -0
- package/dist/addresses.d.ts +134 -0
- package/dist/addresses.d.ts.map +1 -0
- package/dist/addresses.js +16 -0
- package/dist/addresses.js.map +1 -0
- package/dist/admin-actions.d.ts +367 -0
- package/dist/admin-actions.d.ts.map +1 -0
- package/dist/admin-actions.js +287 -0
- package/dist/admin-actions.js.map +1 -0
- package/dist/admin-contributions.d.ts +518 -0
- package/dist/admin-contributions.d.ts.map +1 -0
- package/dist/admin-contributions.js +495 -0
- package/dist/admin-contributions.js.map +1 -0
- package/dist/admin-i18n.d.ts +135 -0
- package/dist/admin-i18n.d.ts.map +1 -0
- package/dist/admin-i18n.js +72 -0
- package/dist/admin-i18n.js.map +1 -0
- package/dist/admin-notifications.d.ts +55 -0
- package/dist/admin-notifications.d.ts.map +1 -0
- package/dist/admin-notifications.js +16 -0
- package/dist/admin-notifications.js.map +1 -0
- package/dist/admin-roles.d.ts +125 -0
- package/dist/admin-roles.d.ts.map +1 -0
- package/dist/admin-roles.js +2 -0
- package/dist/admin-roles.js.map +1 -0
- package/dist/admin-users.d.ts +178 -0
- package/dist/admin-users.d.ts.map +1 -0
- package/dist/admin-users.js +14 -0
- package/dist/admin-users.js.map +1 -0
- package/dist/admin.d.ts +243 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +246 -0
- package/dist/admin.js.map +1 -0
- package/dist/analytics.d.ts +123 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +68 -0
- package/dist/analytics.js.map +1 -0
- package/dist/api-keys.d.ts +97 -0
- package/dist/api-keys.d.ts.map +1 -0
- package/dist/api-keys.js +64 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/assets-library.d.ts +684 -0
- package/dist/assets-library.d.ts.map +1 -0
- package/dist/assets-library.js +181 -0
- package/dist/assets-library.js.map +1 -0
- package/dist/audit-logs.d.ts +141 -0
- package/dist/audit-logs.d.ts.map +1 -0
- package/dist/audit-logs.js +31 -0
- package/dist/audit-logs.js.map +1 -0
- package/dist/auth.d.ts +174 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/blog.d.ts +669 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +360 -0
- package/dist/blog.js.map +1 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +38 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/carts.d.ts +1367 -0
- package/dist/carts.d.ts.map +1 -0
- package/dist/carts.js +405 -0
- package/dist/carts.js.map +1 -0
- package/dist/catalog.d.ts +2855 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +1543 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cms.d.ts +872 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +468 -0
- package/dist/cms.js.map +1 -0
- package/dist/common.d.ts +82 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +72 -0
- package/dist/common.js.map +1 -0
- package/dist/comparisons.d.ts +487 -0
- package/dist/comparisons.d.ts.map +1 -0
- package/dist/comparisons.js +221 -0
- package/dist/comparisons.js.map +1 -0
- package/dist/credentials.d.ts +292 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +142 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credit-limits.d.ts +111 -0
- package/dist/credit-limits.d.ts.map +1 -0
- package/dist/credit-limits.js +35 -0
- package/dist/credit-limits.js.map +1 -0
- package/dist/currencies.d.ts +127 -0
- package/dist/currencies.d.ts.map +1 -0
- package/dist/currencies.js +20 -0
- package/dist/currencies.js.map +1 -0
- package/dist/custom-fields.d.ts +345 -0
- package/dist/custom-fields.d.ts.map +1 -0
- package/dist/custom-fields.js +185 -0
- package/dist/custom-fields.js.map +1 -0
- package/dist/customer-accounts.d.ts +690 -0
- package/dist/customer-accounts.d.ts.map +1 -0
- package/dist/customer-accounts.js +41 -0
- package/dist/customer-accounts.js.map +1 -0
- package/dist/customers.d.ts +305 -0
- package/dist/customers.d.ts.map +1 -0
- package/dist/customers.js +158 -0
- package/dist/customers.js.map +1 -0
- package/dist/dictionary.d.ts +580 -0
- package/dist/dictionary.d.ts.map +1 -0
- package/dist/dictionary.js +297 -0
- package/dist/dictionary.js.map +1 -0
- package/dist/email-address.d.ts +62 -0
- package/dist/email-address.d.ts.map +1 -0
- package/dist/email-address.js +64 -0
- package/dist/email-address.js.map +1 -0
- package/dist/email.d.ts +175 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +45 -0
- package/dist/email.js.map +1 -0
- package/dist/envelopes.d.ts +15 -0
- package/dist/envelopes.d.ts.map +1 -0
- package/dist/envelopes.js +16 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/environment-inputs.d.ts +306 -0
- package/dist/environment-inputs.d.ts.map +1 -0
- package/dist/environment-inputs.js +277 -0
- package/dist/environment-inputs.js.map +1 -0
- package/dist/erp-connector.d.ts +52 -0
- package/dist/erp-connector.d.ts.map +1 -0
- package/dist/erp-connector.js +34 -0
- package/dist/erp-connector.js.map +1 -0
- package/dist/errors.d.ts +455 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +532 -0
- package/dist/errors.js.map +1 -0
- package/dist/google-analytics.d.ts +181 -0
- package/dist/google-analytics.d.ts.map +1 -0
- package/dist/google-analytics.js +176 -0
- package/dist/google-analytics.js.map +1 -0
- package/dist/google-tag-manager.d.ts +111 -0
- package/dist/google-tag-manager.d.ts.map +1 -0
- package/dist/google-tag-manager.js +129 -0
- package/dist/google-tag-manager.js.map +1 -0
- package/dist/i18n.d.ts +69 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +59 -0
- package/dist/i18n.js.map +1 -0
- package/dist/import-export.d.ts +63 -0
- package/dist/import-export.d.ts.map +1 -0
- package/dist/import-export.js +37 -0
- package/dist/import-export.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +673 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +412 -0
- package/dist/inventory.js.map +1 -0
- package/dist/invoice-ledger.d.ts +366 -0
- package/dist/invoice-ledger.d.ts.map +1 -0
- package/dist/invoice-ledger.js +114 -0
- package/dist/invoice-ledger.js.map +1 -0
- package/dist/invoices.d.ts +845 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +314 -0
- package/dist/invoices.js.map +1 -0
- package/dist/kernel.d.ts +49 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +19 -0
- package/dist/kernel.js.map +1 -0
- package/dist/languages.d.ts +122 -0
- package/dist/languages.d.ts.map +1 -0
- package/dist/languages.js +24 -0
- package/dist/languages.js.map +1 -0
- package/dist/linkedin-ads.d.ts +167 -0
- package/dist/linkedin-ads.d.ts.map +1 -0
- package/dist/linkedin-ads.js +156 -0
- package/dist/linkedin-ads.js.map +1 -0
- package/dist/megamenu.d.ts +556 -0
- package/dist/megamenu.d.ts.map +1 -0
- package/dist/megamenu.js +186 -0
- package/dist/megamenu.js.map +1 -0
- package/dist/meta-ads.d.ts +126 -0
- package/dist/meta-ads.d.ts.map +1 -0
- package/dist/meta-ads.js +112 -0
- package/dist/meta-ads.js.map +1 -0
- package/dist/mfa.d.ts +274 -0
- package/dist/mfa.d.ts.map +1 -0
- package/dist/mfa.js +187 -0
- package/dist/mfa.js.map +1 -0
- package/dist/modules.d.ts +1706 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +1390 -0
- package/dist/modules.js.map +1 -0
- package/dist/newsletter.d.ts +611 -0
- package/dist/newsletter.d.ts.map +1 -0
- package/dist/newsletter.js +345 -0
- package/dist/newsletter.js.map +1 -0
- package/dist/orders.d.ts +1175 -0
- package/dist/orders.d.ts.map +1 -0
- package/dist/orders.js +630 -0
- package/dist/orders.js.map +1 -0
- package/dist/organizations.d.ts +938 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +418 -0
- package/dist/organizations.js.map +1 -0
- package/dist/pagination.d.ts +21 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +22 -0
- package/dist/pagination.js.map +1 -0
- package/dist/payment-methods.d.ts +472 -0
- package/dist/payment-methods.d.ts.map +1 -0
- package/dist/payment-methods.js +175 -0
- package/dist/payment-methods.js.map +1 -0
- package/dist/payment-return-url.d.ts +53 -0
- package/dist/payment-return-url.d.ts.map +1 -0
- package/dist/payment-return-url.js +35 -0
- package/dist/payment-return-url.js.map +1 -0
- package/dist/payments.d.ts +386 -0
- package/dist/payments.d.ts.map +1 -0
- package/dist/payments.js +84 -0
- package/dist/payments.js.map +1 -0
- package/dist/pim-connector.d.ts +60 -0
- package/dist/pim-connector.d.ts.map +1 -0
- package/dist/pim-connector.js +43 -0
- package/dist/pim-connector.js.map +1 -0
- package/dist/pim-field-path.d.ts +6 -0
- package/dist/pim-field-path.d.ts.map +1 -0
- package/dist/pim-field-path.js +101 -0
- package/dist/pim-field-path.js.map +1 -0
- package/dist/platform-language.d.ts +20 -0
- package/dist/platform-language.d.ts.map +1 -0
- package/dist/platform-language.js +22 -0
- package/dist/platform-language.js.map +1 -0
- package/dist/price-lists.d.ts +685 -0
- package/dist/price-lists.d.ts.map +1 -0
- package/dist/price-lists.js +330 -0
- package/dist/price-lists.js.map +1 -0
- package/dist/product-feeds.d.ts +2837 -0
- package/dist/product-feeds.d.ts.map +1 -0
- package/dist/product-feeds.js +1504 -0
- package/dist/product-feeds.js.map +1 -0
- package/dist/product-scope-overrides.d.ts +134 -0
- package/dist/product-scope-overrides.d.ts.map +1 -0
- package/dist/product-scope-overrides.js +82 -0
- package/dist/product-scope-overrides.js.map +1 -0
- package/dist/product-value-resolver.d.ts +88 -0
- package/dist/product-value-resolver.d.ts.map +1 -0
- package/dist/product-value-resolver.js +128 -0
- package/dist/product-value-resolver.js.map +1 -0
- package/dist/promotions.d.ts +678 -0
- package/dist/promotions.d.ts.map +1 -0
- package/dist/promotions.js +479 -0
- package/dist/promotions.js.map +1 -0
- package/dist/prompt-actions.d.ts +582 -0
- package/dist/prompt-actions.d.ts.map +1 -0
- package/dist/prompt-actions.js +221 -0
- package/dist/prompt-actions.js.map +1 -0
- package/dist/pwa.d.ts +293 -0
- package/dist/pwa.d.ts.map +1 -0
- package/dist/pwa.js +204 -0
- package/dist/pwa.js.map +1 -0
- package/dist/quick-order.d.ts +340 -0
- package/dist/quick-order.d.ts.map +1 -0
- package/dist/quick-order.js +177 -0
- package/dist/quick-order.js.map +1 -0
- package/dist/quote-requests.d.ts +538 -0
- package/dist/quote-requests.d.ts.map +1 -0
- package/dist/quote-requests.js +308 -0
- package/dist/quote-requests.js.map +1 -0
- package/dist/returns.d.ts +774 -0
- package/dist/returns.d.ts.map +1 -0
- package/dist/returns.js +389 -0
- package/dist/returns.js.map +1 -0
- package/dist/sales-channels.d.ts +392 -0
- package/dist/sales-channels.d.ts.map +1 -0
- package/dist/sales-channels.js +285 -0
- package/dist/sales-channels.js.map +1 -0
- package/dist/scope-notice.d.ts +60 -0
- package/dist/scope-notice.d.ts.map +1 -0
- package/dist/scope-notice.js +56 -0
- package/dist/scope-notice.js.map +1 -0
- package/dist/search.d.ts +321 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +160 -0
- package/dist/search.js.map +1 -0
- package/dist/seo.d.ts +113 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +63 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings.d.ts +453 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +337 -0
- package/dist/settings.js.map +1 -0
- package/dist/shipments.d.ts +140 -0
- package/dist/shipments.d.ts.map +1 -0
- package/dist/shipments.js +14 -0
- package/dist/shipments.js.map +1 -0
- package/dist/shipping-methods.d.ts +350 -0
- package/dist/shipping-methods.d.ts.map +1 -0
- package/dist/shipping-methods.js +99 -0
- package/dist/shipping-methods.js.map +1 -0
- package/dist/shopping-lists.d.ts +122 -0
- package/dist/shopping-lists.d.ts.map +1 -0
- package/dist/shopping-lists.js +92 -0
- package/dist/shopping-lists.js.map +1 -0
- package/dist/taxes.d.ts +106 -0
- package/dist/taxes.d.ts.map +1 -0
- package/dist/taxes.js +80 -0
- package/dist/taxes.js.map +1 -0
- package/dist/text-normalization.d.ts +199 -0
- package/dist/text-normalization.d.ts.map +1 -0
- package/dist/text-normalization.js +205 -0
- package/dist/text-normalization.js.map +1 -0
- package/dist/transactional-emails.d.ts +459 -0
- package/dist/transactional-emails.d.ts.map +1 -0
- package/dist/transactional-emails.js +212 -0
- package/dist/transactional-emails.js.map +1 -0
- package/dist/webhooks.d.ts +69 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +53 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { isoDateTimeSchema, uuidSchema } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shopping list contracts (T202 / FR-031..FR-033).
|
|
5
|
+
*
|
|
6
|
+
* A shopping list is a per-customer, named collection of (product, variant?, quantity)
|
|
7
|
+
* rows with optional notes. Two terminal actions:
|
|
8
|
+
* - convertToCart — adds the (selected or all) items to the customer's active cart;
|
|
9
|
+
* archived products are skipped and reported.
|
|
10
|
+
* - convertToRfq — adds the (selected or all) items to the customer's draft RFQ;
|
|
11
|
+
* same skip-and-report behaviour.
|
|
12
|
+
*
|
|
13
|
+
* Lists are private to the (customer, organization) pair today; sharing within an
|
|
14
|
+
* organization is intentionally out of scope for the MVP — the entity has no
|
|
15
|
+
* `isShared` column yet to keep the surface narrow.
|
|
16
|
+
*/
|
|
17
|
+
export const shoppingListItemSchema = z.object({
|
|
18
|
+
id: uuidSchema,
|
|
19
|
+
shoppingListId: uuidSchema,
|
|
20
|
+
productId: uuidSchema,
|
|
21
|
+
variantId: uuidSchema.nullable(),
|
|
22
|
+
quantity: z.number().int().positive(),
|
|
23
|
+
note: z.string().max(2000).nullable(),
|
|
24
|
+
createdAt: isoDateTimeSchema,
|
|
25
|
+
updatedAt: isoDateTimeSchema,
|
|
26
|
+
});
|
|
27
|
+
export const shoppingListSchema = z.object({
|
|
28
|
+
id: uuidSchema,
|
|
29
|
+
organizationId: z.string(),
|
|
30
|
+
customerAccountId: uuidSchema,
|
|
31
|
+
name: z.string().min(1).max(160),
|
|
32
|
+
/** Whether this is the customer's default shopping list (exactly one per customer). */
|
|
33
|
+
isDefault: z.boolean(),
|
|
34
|
+
items: z.array(shoppingListItemSchema),
|
|
35
|
+
createdAt: isoDateTimeSchema,
|
|
36
|
+
updatedAt: isoDateTimeSchema,
|
|
37
|
+
});
|
|
38
|
+
/** Lightweight default-list view for the storefront header heart badge. */
|
|
39
|
+
export const defaultShoppingListSummarySchema = z.object({
|
|
40
|
+
id: uuidSchema,
|
|
41
|
+
name: z.string(),
|
|
42
|
+
itemCount: z.number().int().nonnegative(),
|
|
43
|
+
});
|
|
44
|
+
// --- Requests --------------------------------------------------------------
|
|
45
|
+
export const createShoppingListRequestSchema = z.object({
|
|
46
|
+
name: z.string().min(1).max(160),
|
|
47
|
+
});
|
|
48
|
+
export const renameShoppingListRequestSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
name: z.string().min(1).max(160),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
export const addShoppingListItemRequestSchema = z.object({
|
|
54
|
+
productId: uuidSchema,
|
|
55
|
+
variantId: uuidSchema.optional(),
|
|
56
|
+
quantity: z.number().int().positive(),
|
|
57
|
+
note: z.string().max(2000).optional(),
|
|
58
|
+
});
|
|
59
|
+
export const updateShoppingListItemRequestSchema = z
|
|
60
|
+
.object({
|
|
61
|
+
quantity: z.number().int().positive().optional(),
|
|
62
|
+
note: z.string().max(2000).nullable().optional(),
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
/**
|
|
66
|
+
* The convert request optionally narrows by item id; an empty (or missing)
|
|
67
|
+
* `itemIds` array means "convert every item on the list".
|
|
68
|
+
*/
|
|
69
|
+
export const convertShoppingListRequestSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
itemIds: z.array(uuidSchema).optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
// --- Responses -------------------------------------------------------------
|
|
75
|
+
/** Per-item skip note returned when conversion encounters an archived product. */
|
|
76
|
+
export const conversionSkipSchema = z.object({
|
|
77
|
+
itemId: uuidSchema,
|
|
78
|
+
productId: uuidSchema,
|
|
79
|
+
reason: z.enum(['product_archived', 'variant_unavailable', 'product_not_found']),
|
|
80
|
+
});
|
|
81
|
+
export const convertToCartResponseSchema = z.object({
|
|
82
|
+
shoppingListId: uuidSchema,
|
|
83
|
+
added: z.number().int().nonnegative(),
|
|
84
|
+
skipped: z.array(conversionSkipSchema),
|
|
85
|
+
});
|
|
86
|
+
export const convertToRfqResponseSchema = z.object({
|
|
87
|
+
shoppingListId: uuidSchema,
|
|
88
|
+
rfqId: uuidSchema,
|
|
89
|
+
added: z.number().int().nonnegative(),
|
|
90
|
+
skipped: z.array(conversionSkipSchema),
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=shopping-lists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shopping-lists.js","sourceRoot":"","sources":["../src/shopping-lists.ts"],"names":[],"mappings":"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"}
|
package/dist/taxes.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const taxSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
code: z.ZodString;
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
rate: z.ZodNumber;
|
|
7
|
+
country: z.ZodNullable<z.ZodString>;
|
|
8
|
+
productType: z.ZodNullable<z.ZodEnum<{
|
|
9
|
+
simple: "simple";
|
|
10
|
+
configurable: "configurable";
|
|
11
|
+
grouped: "grouped";
|
|
12
|
+
bundle: "bundle";
|
|
13
|
+
virtual: "virtual";
|
|
14
|
+
}>>;
|
|
15
|
+
appliesToVatStatuses: z.ZodArray<z.ZodEnum<{
|
|
16
|
+
vat_payer: "vat_payer";
|
|
17
|
+
vat_exempt: "vat_exempt";
|
|
18
|
+
reverse_charge: "reverse_charge";
|
|
19
|
+
}>>;
|
|
20
|
+
isDefault: z.ZodBoolean;
|
|
21
|
+
priority: z.ZodNumber;
|
|
22
|
+
createdAt: z.ZodString;
|
|
23
|
+
updatedAt: z.ZodString;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type Tax = z.infer<typeof taxSchema>;
|
|
26
|
+
export declare const upsertTaxRequestSchema: z.ZodObject<{
|
|
27
|
+
code: z.ZodString;
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
rate: z.ZodNumber;
|
|
30
|
+
country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
|
+
productType: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
32
|
+
simple: "simple";
|
|
33
|
+
configurable: "configurable";
|
|
34
|
+
grouped: "grouped";
|
|
35
|
+
bundle: "bundle";
|
|
36
|
+
virtual: "virtual";
|
|
37
|
+
}>>>;
|
|
38
|
+
appliesToVatStatuses: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
39
|
+
vat_payer: "vat_payer";
|
|
40
|
+
vat_exempt: "vat_exempt";
|
|
41
|
+
reverse_charge: "reverse_charge";
|
|
42
|
+
}>>>;
|
|
43
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
44
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
export declare const taxResolutionInputSchema: z.ZodObject<{
|
|
47
|
+
country: z.ZodString;
|
|
48
|
+
productType: z.ZodEnum<{
|
|
49
|
+
simple: "simple";
|
|
50
|
+
configurable: "configurable";
|
|
51
|
+
grouped: "grouped";
|
|
52
|
+
bundle: "bundle";
|
|
53
|
+
virtual: "virtual";
|
|
54
|
+
}>;
|
|
55
|
+
vatStatus: z.ZodEnum<{
|
|
56
|
+
vat_payer: "vat_payer";
|
|
57
|
+
vat_exempt: "vat_exempt";
|
|
58
|
+
reverse_charge: "reverse_charge";
|
|
59
|
+
}>;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
export type TaxResolutionInput = z.infer<typeof taxResolutionInputSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* The resolver's answer, as a union rather than a record with a `rate` that is
|
|
64
|
+
* sometimes meaningless (issue #124).
|
|
65
|
+
*
|
|
66
|
+
* A **configured** 0% rate is a legitimate answer — zero-rated supplies exist —
|
|
67
|
+
* so `rule` and `default` carry a `rate` that a caller may spend. "No rule
|
|
68
|
+
* matched and no default is configured" is not an answer at all, so the `none`
|
|
69
|
+
* arm carries **no `rate` field**: a caller has to narrow on `source` before it
|
|
70
|
+
* can read a number, and therefore has to decide, in the open, what its own
|
|
71
|
+
* surface does about it.
|
|
72
|
+
*
|
|
73
|
+
* `{ rate: 0, source: 'none' }` was the previous shape and it collapsed exactly
|
|
74
|
+
* that distinction — every consumer read `.rate`, got `0`, and quoted a zero-VAT
|
|
75
|
+
* figure nobody had configured onto documents that had already gone out.
|
|
76
|
+
*
|
|
77
|
+
* A third state, "the `taxes` module is absent", is deliberately **not** in this
|
|
78
|
+
* union: absence is not a value. The port gate throws `MODULE_DISABLED` before a
|
|
79
|
+
* resolution runs, so a caller never has to tell an absent owner from a silent
|
|
80
|
+
* one.
|
|
81
|
+
*/
|
|
82
|
+
export declare const resolvedTaxSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
83
|
+
source: z.ZodLiteral<"rule">;
|
|
84
|
+
rate: z.ZodNumber;
|
|
85
|
+
taxId: z.ZodString;
|
|
86
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
87
|
+
source: z.ZodLiteral<"default">;
|
|
88
|
+
rate: z.ZodNumber;
|
|
89
|
+
taxId: z.ZodString;
|
|
90
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
91
|
+
source: z.ZodLiteral<"none">;
|
|
92
|
+
}, z.core.$strip>], "source">;
|
|
93
|
+
export type ResolvedTax = z.infer<typeof resolvedTaxSchema>;
|
|
94
|
+
/**
|
|
95
|
+
* Container name: `taxService`. Owner: `taxes`.
|
|
96
|
+
*
|
|
97
|
+
* `ResolvedTax`'s `none` arm is the reason this shape is a union rather than a
|
|
98
|
+
* number: "no rule matched" is a real answer with real consequences, and a
|
|
99
|
+
* caller that read a `rate` off the `none` arm would get `undefined` and quote
|
|
100
|
+
* zero. Issue #124 was exactly that, in `orders`' closure, and the union is
|
|
101
|
+
* what makes the compiler refuse it.
|
|
102
|
+
*/
|
|
103
|
+
export interface TaxServicePort {
|
|
104
|
+
taxRateFor(input: TaxResolutionInput): Promise<ResolvedTax>;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=taxes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxes.d.ts","sourceRoot":"","sources":["../src/taxes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;iBAYpB,CAAC;AACH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAE5C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;iBAYjC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;iBAInC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;6BAY5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAU5D;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAC7D"}
|
package/dist/taxes.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { isoDateTimeSchema, uuidSchema } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tax contracts (T128, T131 / FR-051).
|
|
5
|
+
*
|
|
6
|
+
* Each rule narrows by zero or more of:
|
|
7
|
+
* - `country` (delivery country, ISO 3166-1 alpha-2)
|
|
8
|
+
* - `productType` (the catalog Product.type — simple/variant/grouped/virtual)
|
|
9
|
+
* - `appliesToVatStatuses` (Organization.vatStatus values that match)
|
|
10
|
+
*
|
|
11
|
+
* Resolution picks the most specific matching rule (most narrowed
|
|
12
|
+
* conditions). Ties fall back to the rule with `priority` highest;
|
|
13
|
+
* when no rule matches, the default rule (if any) is used.
|
|
14
|
+
*/
|
|
15
|
+
const VAT_STATUS = z.enum(['vat_payer', 'vat_exempt', 'reverse_charge']);
|
|
16
|
+
export const taxSchema = z.object({
|
|
17
|
+
id: uuidSchema,
|
|
18
|
+
code: z.string().min(1).max(64),
|
|
19
|
+
name: z.string().min(1).max(160),
|
|
20
|
+
rate: z.number().finite().nonnegative(),
|
|
21
|
+
country: z.string().length(2).nullable(),
|
|
22
|
+
productType: z.enum(['simple', 'configurable', 'grouped', 'bundle', 'virtual']).nullable(),
|
|
23
|
+
appliesToVatStatuses: z.array(VAT_STATUS),
|
|
24
|
+
isDefault: z.boolean(),
|
|
25
|
+
priority: z.number().int(),
|
|
26
|
+
createdAt: isoDateTimeSchema,
|
|
27
|
+
updatedAt: isoDateTimeSchema,
|
|
28
|
+
});
|
|
29
|
+
export const upsertTaxRequestSchema = z.object({
|
|
30
|
+
code: z.string().min(1).max(64),
|
|
31
|
+
name: z.string().min(1).max(160),
|
|
32
|
+
rate: z.number().finite().nonnegative(),
|
|
33
|
+
country: z.string().length(2).nullable().optional(),
|
|
34
|
+
productType: z
|
|
35
|
+
.enum(['simple', 'configurable', 'grouped', 'bundle', 'virtual'])
|
|
36
|
+
.nullable()
|
|
37
|
+
.optional(),
|
|
38
|
+
appliesToVatStatuses: z.array(VAT_STATUS).optional(),
|
|
39
|
+
isDefault: z.boolean().optional(),
|
|
40
|
+
priority: z.number().int().optional(),
|
|
41
|
+
});
|
|
42
|
+
export const taxResolutionInputSchema = z.object({
|
|
43
|
+
country: z.string().length(2),
|
|
44
|
+
productType: z.enum(['simple', 'configurable', 'grouped', 'bundle', 'virtual']),
|
|
45
|
+
vatStatus: VAT_STATUS,
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* The resolver's answer, as a union rather than a record with a `rate` that is
|
|
49
|
+
* sometimes meaningless (issue #124).
|
|
50
|
+
*
|
|
51
|
+
* A **configured** 0% rate is a legitimate answer — zero-rated supplies exist —
|
|
52
|
+
* so `rule` and `default` carry a `rate` that a caller may spend. "No rule
|
|
53
|
+
* matched and no default is configured" is not an answer at all, so the `none`
|
|
54
|
+
* arm carries **no `rate` field**: a caller has to narrow on `source` before it
|
|
55
|
+
* can read a number, and therefore has to decide, in the open, what its own
|
|
56
|
+
* surface does about it.
|
|
57
|
+
*
|
|
58
|
+
* `{ rate: 0, source: 'none' }` was the previous shape and it collapsed exactly
|
|
59
|
+
* that distinction — every consumer read `.rate`, got `0`, and quoted a zero-VAT
|
|
60
|
+
* figure nobody had configured onto documents that had already gone out.
|
|
61
|
+
*
|
|
62
|
+
* A third state, "the `taxes` module is absent", is deliberately **not** in this
|
|
63
|
+
* union: absence is not a value. The port gate throws `MODULE_DISABLED` before a
|
|
64
|
+
* resolution runs, so a caller never has to tell an absent owner from a silent
|
|
65
|
+
* one.
|
|
66
|
+
*/
|
|
67
|
+
export const resolvedTaxSchema = z.discriminatedUnion('source', [
|
|
68
|
+
z.object({
|
|
69
|
+
source: z.literal('rule'),
|
|
70
|
+
rate: z.number().finite().nonnegative(),
|
|
71
|
+
taxId: uuidSchema,
|
|
72
|
+
}),
|
|
73
|
+
z.object({
|
|
74
|
+
source: z.literal('default'),
|
|
75
|
+
rate: z.number().finite().nonnegative(),
|
|
76
|
+
taxId: uuidSchema,
|
|
77
|
+
}),
|
|
78
|
+
z.object({ source: z.literal('none') }),
|
|
79
|
+
]);
|
|
80
|
+
//# sourceMappingURL=taxes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxes.js","sourceRoot":"","sources":["../src/taxes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One diacritic fold — and, since issue #245, one slug generator — for every
|
|
3
|
+
* package that has to compare or key two spellings of the same word.
|
|
4
|
+
*
|
|
5
|
+
* `foldDiacritics` is the fold (issue #240). `slugify` is the generator built
|
|
6
|
+
* on it, and it replaced **eight** private copies; its own doc block carries
|
|
7
|
+
* the reasons that are specific to slugs. Everything below is about the fold.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this file exists rather than a helper inside one consumer
|
|
10
|
+
*
|
|
11
|
+
* The fold was correct and unreachable. It lived inside
|
|
12
|
+
* `normalizeOrganizationName` in `organizations.ts` — published from
|
|
13
|
+
* `@endora-commerce/contracts`, so importable from `backend/`, `admin/` and `storefront/`
|
|
14
|
+
* alike, but named after **one caller**. Nobody looking for "how do I compare
|
|
15
|
+
* `Łatwy` with `latwy`" searches for an organisation helper, so within a year
|
|
16
|
+
* four private copies had grown in `admin/` (repaired by !745) and two more
|
|
17
|
+
* stand in `backend/` today. Every one of them was the same one-liner and every
|
|
18
|
+
* one of them shipped the same bug.
|
|
19
|
+
*
|
|
20
|
+
* ## The bug the one-liner has
|
|
21
|
+
*
|
|
22
|
+
* ```
|
|
23
|
+
* input.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase()
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* reads as complete and is not. `ł` is U+0142: a single indivisible code point
|
|
27
|
+
* with **no canonical decomposition**, so NFD leaves it exactly where it was,
|
|
28
|
+
* the strip finds no combining mark to remove, and the `[^a-z0-9]+` pass a
|
|
29
|
+
* slugifier runs next deletes the letter outright — `Łatwy szablon` becomes
|
|
30
|
+
* `atwy-szablon`. The same holds for `Ø ø Đ đ Ð ð Þ þ ß Æ æ Œ œ`. The fix is one
|
|
31
|
+
* extra step, and it is the only reason this is longer than a line: map those
|
|
32
|
+
* letters explicitly.
|
|
33
|
+
*
|
|
34
|
+
* ## A function, not a port
|
|
35
|
+
*
|
|
36
|
+
* It is pure over its argument, so it cannot answer differently depending on
|
|
37
|
+
* which modules are switched on, and a gated port answering 503 `MODULE_DISABLED`
|
|
38
|
+
* to "fold these diacritics" would be a bug rather than a safety property.
|
|
39
|
+
* Node natives only (Principle IV) — a full Unicode-folding library is not worth
|
|
40
|
+
* a dependency for fifteen letters.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Latin letters NFD does not decompose, and their ASCII approximations.
|
|
44
|
+
*
|
|
45
|
+
* Every entry is a **single code point** whose Unicode decomposition is empty:
|
|
46
|
+
* a stroked, hooked or ligated letter rather than a base plus a combining mark.
|
|
47
|
+
* That is precisely the set NFD cannot help with, which is why it is hand-written.
|
|
48
|
+
*
|
|
49
|
+
* Exported so a test can enumerate it and a reader can see the whole set at
|
|
50
|
+
* once — not as an invitation to write a second fold around it. Reach for
|
|
51
|
+
* `foldDiacritics` instead; a copy of the map without the decomposition step
|
|
52
|
+
* is half a fold, and half a fold is what this file exists to end.
|
|
53
|
+
*/
|
|
54
|
+
export declare const NON_DECOMPOSING_LATIN: Readonly<Record<string, string>>;
|
|
55
|
+
/**
|
|
56
|
+
* Fold a string so two spellings of the same word compare equal: diacritics
|
|
57
|
+
* removed, non-decomposing Latin letters mapped to ASCII, lowercased.
|
|
58
|
+
*
|
|
59
|
+
* In order:
|
|
60
|
+
*
|
|
61
|
+
* 1. **Decompose (NFD)** — separate every base character from its combining
|
|
62
|
+
* marks, so `ó` becomes `o` + U+0301.
|
|
63
|
+
* 2. **Strip the combining marks** via `\p{Diacritic}`.
|
|
64
|
+
* 3. **Map what NFD could not decompose**, per `NON_DECOMPOSING_LATIN`. This
|
|
65
|
+
* step runs **after** the decomposition on purpose: `Ǿ` (U+01FE) decomposes
|
|
66
|
+
* to `Ø` + U+0301, so a map applied first would not see the `Ø` that step 1
|
|
67
|
+
* is about to expose. Mapping first — as `admin/`'s copy did until issue
|
|
68
|
+
* #240 — folds `Ǿ` to `ø` instead of `o`.
|
|
69
|
+
* 4. **Lowercase.** Callers compare case-insensitively without exception, and
|
|
70
|
+
* the ASCII expansions above (`Th`, `AE`, `OE`) have to be lowered anyway.
|
|
71
|
+
*
|
|
72
|
+
* Whitespace is deliberately **not** touched: what counts as one space is the
|
|
73
|
+
* caller's policy, not the fold's. `normalizeOrganizationName` collapses runs
|
|
74
|
+
* and trims because it writes a search column; `admin`'s `normalize` only
|
|
75
|
+
* trims. Both compose it here rather than each folding its own way.
|
|
76
|
+
*
|
|
77
|
+
* Not a case-folding routine in the Unicode sense: it lowercases, it does not
|
|
78
|
+
* apply full case folding, and anything outside Latin (Cyrillic, CJK, Greek)
|
|
79
|
+
* passes through with its case lowered and nothing else changed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function foldDiacritics(input: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* How one caller's slug grammar differs from another's. Every field is the
|
|
84
|
+
* caller's own policy; the *steps* are not negotiable and are not options.
|
|
85
|
+
*/
|
|
86
|
+
export interface SlugifyOptions {
|
|
87
|
+
/**
|
|
88
|
+
* What a run of unusable characters collapses to. `-` everywhere except the
|
|
89
|
+
* newsletter tag code, whose stored grammar is `_`.
|
|
90
|
+
*/
|
|
91
|
+
readonly separator?: string;
|
|
92
|
+
/**
|
|
93
|
+
* The caller's own column or contract limit. **Deliberately not defaulted to
|
|
94
|
+
* a repo-wide number**: eight call sites cap at 80, 150, 160, 180 or not at
|
|
95
|
+
* all, and each cap decides which *new* values collide under that caller's
|
|
96
|
+
* unique constraint. Normalising them would be a data decision dressed as a
|
|
97
|
+
* tidy-up.
|
|
98
|
+
*/
|
|
99
|
+
readonly maxLength?: number;
|
|
100
|
+
/** What an input that folds away to nothing produces. Empty string by default. */
|
|
101
|
+
readonly fallback?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Characters the caller's grammar allows **besides** `[a-z0-9]`, which
|
|
104
|
+
* therefore survive instead of collapsing to the separator.
|
|
105
|
+
*
|
|
106
|
+
* Empty for every slug in the kebab family, and that is the reason this is an
|
|
107
|
+
* option rather than a widening of the rule: `pim_ergonode`'s option values
|
|
108
|
+
* are stored under `^[a-z0-9_-]{1,200}$`, a grammar with **two** usable
|
|
109
|
+
* punctuation characters, so a generator that knows only one separator cannot
|
|
110
|
+
* express it. Without this, folding those values correctly would also have
|
|
111
|
+
* turned every already-correct `xl-red` into `xl_red` — a rename with no
|
|
112
|
+
* defect behind it, and one that orphans the option every product already
|
|
113
|
+
* points at (issue #260).
|
|
114
|
+
*
|
|
115
|
+
* Members are matched literally inside a negated character class, so a
|
|
116
|
+
* caller passes the characters themselves (`'-'`), not an escaped class.
|
|
117
|
+
*/
|
|
118
|
+
readonly preserve?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The repository's one slug generator (issue #245).
|
|
122
|
+
*
|
|
123
|
+
* There were **eight**: three in `backend/`, five in `admin/`, each a private
|
|
124
|
+
* four-line chain that read as obviously right. Two of them
|
|
125
|
+
* (`cms-template-layout.ts`, `pim_ergonode`'s `slugFromSourceCode`) had no fold
|
|
126
|
+
* step at all — `Żółw` produced `w` and `Świeże Ćwikła` produced `wie-e-wik-a`
|
|
127
|
+
* — and `check:diacritic-folds` was structurally blind to both, because it
|
|
128
|
+
* counts folds written *outside* the shared helper and a site that folds
|
|
129
|
+
* nothing writes none. A shared generator is what makes "does this slug fold?"
|
|
130
|
+
* a question with one answer.
|
|
131
|
+
*
|
|
132
|
+
* The steps, in order:
|
|
133
|
+
*
|
|
134
|
+
* 1. **Fold** via `foldDiacritics` — decompose, strip the combining marks, map
|
|
135
|
+
* the letters NFD leaves standing, lowercase.
|
|
136
|
+
* 2. **Collapse** every run of `[^a-z0-9]` — minus whatever `preserve` adds to
|
|
137
|
+
* the usable set — to the separator.
|
|
138
|
+
* 3. **Strip a leading separator**, before the slice — a leading separator can
|
|
139
|
+
* never survive, so it must not spend a character of the caller's budget.
|
|
140
|
+
* 4. **Slice** to `maxLength`, when the caller has one.
|
|
141
|
+
* 5. **Strip trailing separators**, after the slice — the cut can *create* one,
|
|
142
|
+
* which is the whole point (see below).
|
|
143
|
+
* 6. **Fall back** when nothing survived.
|
|
144
|
+
*
|
|
145
|
+
* ## NFD, not NFKD, and that is a decision
|
|
146
|
+
*
|
|
147
|
+
* Three of the eight normalised with `NFKD`, which additionally maps the
|
|
148
|
+
* compatibility characters: `fi` to `fi`, `²` to `2`, the full-width forms to
|
|
149
|
+
* their ASCII. Those mappings reach a slug **only** through characters that map
|
|
150
|
+
* *into* `[a-z0-9]`, and none of them is typed into a product name, a feed
|
|
151
|
+
* template name or a CMS page title. Where one does appear it now collapses to
|
|
152
|
+
* the separator rather than to a letter, so the slug stays legal and merely
|
|
153
|
+
* loses a character nobody could see in it. Against that, `ł` was being deleted
|
|
154
|
+
* out of every Polish name those three touched. The trade was taken for the
|
|
155
|
+
* admin pair in !753 and holds identically here.
|
|
156
|
+
*
|
|
157
|
+
* It has one consequence worth naming: two inputs `NFKD` kept apart can now
|
|
158
|
+
* fold together (`Kabel²` and `Kabel³` both become `kabel`). Every caller whose
|
|
159
|
+
* slug is unique-constrained already allocates against the live table —
|
|
160
|
+
* `CatalogAdminService.allocateUniqueSlug`, `pim_ergonode`'s
|
|
161
|
+
* `createWithFreeSlug` — so the second one is suffixed rather than rejected,
|
|
162
|
+
* exactly as two products sharing a plain name already are.
|
|
163
|
+
*
|
|
164
|
+
* ## The trailing strip runs after the slice, which is a change for six sites
|
|
165
|
+
*
|
|
166
|
+
* Six of the eight stripped **both** edge separators before cutting to length,
|
|
167
|
+
* so a cut landing on a separator left the slug ending in one:
|
|
168
|
+
* `...w-wersji-rozszerzona-`. Only `BlockEditor` and `PageEditor` had it right.
|
|
169
|
+
* This is the one change here that is not about diacritics; it shows only on an
|
|
170
|
+
* input long enough to be truncated, and it makes the result satisfy the kebab
|
|
171
|
+
* grammar (`^[a-z0-9]+(?:-[a-z0-9]+)*$`) that `pim_ergonode`'s own doc block
|
|
172
|
+
* already claimed for it.
|
|
173
|
+
*
|
|
174
|
+
* The **leading** strip stays where all eight had it — before the slice — for a
|
|
175
|
+
* reason that is not symmetry: a leading separator is never part of the answer,
|
|
176
|
+
* so letting it consume a character of `maxLength` would shorten every slug
|
|
177
|
+
* whose input begins with punctuation by one, in six sites at once, for no
|
|
178
|
+
* gain. Only the trailing edge can be *created* by the cut, so only the
|
|
179
|
+
* trailing strip has to run after it.
|
|
180
|
+
*
|
|
181
|
+
* Historical values are **not** migrated (owner's ruling, 2026-08-19,
|
|
182
|
+
* originally for the admin pair in !753 and extended to the rest): only two
|
|
183
|
+
* developer environments exist, so re-slugging live rows buys nobody anything,
|
|
184
|
+
* and every one of these values is computed fresh at create time rather than
|
|
185
|
+
* re-derived to look an existing row up. New values are correct from here on.
|
|
186
|
+
*
|
|
187
|
+
* **`pim_ergonode` is the exception the ruling names, and it is the reason
|
|
188
|
+
* `preserve` exists.** Its two derivations are re-derived on every import run
|
|
189
|
+
* *to find the row a previous run created*, so a changed derivation orphans the
|
|
190
|
+
* row rather than improving it. Those values are migrated (issue #260), and the
|
|
191
|
+
* migration is only affordable because the derivation change is confined to the
|
|
192
|
+
* fold — which is what `preserve` buys.
|
|
193
|
+
*
|
|
194
|
+
* `normalizeOrganizationName` is **not** a caller and must not become one: it
|
|
195
|
+
* writes the persisted `organizations.name_search` column, which is a folded
|
|
196
|
+
* name rather than a slug.
|
|
197
|
+
*/
|
|
198
|
+
export declare function slugify(input: string, options?: SlugifyOptions): string;
|
|
199
|
+
//# sourceMappingURL=text-normalization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-normalization.d.ts","sourceRoot":"","sources":["../src/text-normalization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBlE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAmB3E"}
|