@aglyn/plugins-commerce 1.0.0-beta.249 → 1.0.0-beta.252

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 (52) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/commerce-console-page.d.ts +1 -1
  3. package/src/lib/components/commerce-console-page.js +139 -175
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/commerce-console-sections.d.ts +30 -2
  6. package/src/lib/components/commerce-console-sections.js +82 -7
  7. package/src/lib/components/commerce-console-sections.js.map +1 -1
  8. package/src/lib/components/console/discounts-card.component.js +36 -3
  9. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-coupons-card.component.js +1 -1
  11. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  12. package/src/lib/components/console/pos/pos-pairing.component.d.ts +59 -0
  13. package/src/lib/components/console/pos/pos-pairing.component.js +174 -0
  14. package/src/lib/components/console/pos/pos-pairing.component.js.map +1 -0
  15. package/src/lib/components/console/pos/use-pos-display.d.ts +0 -8
  16. package/src/lib/components/console/pos/use-pos-display.js +2 -16
  17. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  18. package/src/lib/components/console/pos-devices-card.component.d.ts +12 -1
  19. package/src/lib/components/console/pos-devices-card.component.js +132 -59
  20. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  21. package/src/lib/components/console/pos-page.component.d.ts +1 -1
  22. package/src/lib/components/console/pos-page.component.js +37 -86
  23. package/src/lib/components/console/pos-page.component.js.map +1 -1
  24. package/src/lib/components/console/registers-card.component.js +13 -0
  25. package/src/lib/components/console/registers-card.component.js.map +1 -1
  26. package/src/lib/components/console/shipping-settings-card.component.js +6 -11
  27. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  28. package/src/lib/components/console/store-zones.d.ts +15 -0
  29. package/src/lib/components/console/store-zones.js +36 -4
  30. package/src/lib/components/console/store-zones.js.map +1 -1
  31. package/src/lib/components/console/suppliers-card.component.js +2 -2
  32. package/src/lib/components/console/suppliers-card.component.js.map +1 -1
  33. package/src/lib/model/commerce-record-routes.js +10 -3
  34. package/src/lib/model/commerce-record-routes.js.map +1 -1
  35. package/src/lib/model/commerce-shipping.d.ts +16 -0
  36. package/src/lib/model/commerce-shipping.js +21 -0
  37. package/src/lib/model/commerce-shipping.js.map +1 -1
  38. package/src/lib/model/commerce-tax-decision.js +2 -2
  39. package/src/lib/model/commerce-tax-decision.js.map +1 -1
  40. package/src/lib/plugin.js +4 -1
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +13 -0
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/order-webhooks.js +1 -1
  45. package/src/lib/server/order-webhooks.js.map +1 -1
  46. package/src/lib/server/shippingeasy.js +1 -1
  47. package/src/lib/server/shippingeasy.js.map +1 -1
  48. package/src/lib/server/store-setup-ai-capability.js +10 -2
  49. package/src/lib/server/store-setup-ai-capability.js.map +1 -1
  50. package/src/lib/server/supplier-routing.d.ts +86 -0
  51. package/src/lib/server/supplier-routing.js +175 -0
  52. package/src/lib/server/supplier-routing.js.map +1 -0
@@ -13,7 +13,27 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ /**
16
+ */ /** The words above the settings pages in the rail (AGL-3713). */ export const COMMERCE_SETTINGS_GROUP = 'Settings';
17
+ /**
18
+ * The zone each settings page draws at its foot, keyed by page (AGL-3713).
19
+ *
20
+ * One zone per page, so a plugin chooses its page by the zone it names and
21
+ * nothing else: a contributor restates the id, never imports this package.
22
+ * `commerceSettings` — the zone every store-settings widget named before the
23
+ * split — is the Integrations page's, so a widget that names no page lands
24
+ * there rather than nowhere.
25
+ */ export const COMMERCE_SETTINGS_PAGE_ZONES = {
26
+ payments: 'commerceSettingsPayments',
27
+ checkout: 'commerceSettingsCheckout',
28
+ taxes: 'commerceSettingsTaxes',
29
+ shipping: 'commerceSettingsShipping',
30
+ notifications: 'commerceSettingsNotifications',
31
+ pos: 'commerceSettingsPos',
32
+ channels: 'commerceSettingsChannels',
33
+ integrations: 'commerceSettings'
34
+ };
35
+ /** Under the "Settings" subheader (AGL-3713). */ const SETTINGS = COMMERCE_SETTINGS_GROUP;
36
+ /**
17
37
  * The commerce console's sections, in rail order (AGL-2501).
18
38
  *
19
39
  * One list, read twice and never copied: `plugin.ts` registers it on the nav
@@ -22,11 +42,15 @@
22
42
  * to be routable under one id and drawn under another.
23
43
  *
24
44
  * Ids appear in links people keep — treat them as persisted, and rename a
25
- * LABEL rather than an id.
45
+ * LABEL rather than an id. An id that has to go is mapped in
46
+ * {@link COMMERCE_RETIRED_SECTIONS} to the section that replaced it.
26
47
  *
27
48
  * No `navTabId` on any of them: every section here ships with the surface, so
28
49
  * they inherit the Products nav item's gate. A section that ships later
29
50
  * declares its own, which ANDs with the parent's rather than replacing it.
51
+ *
52
+ * The store's own work first, then its settings under a divider and a
53
+ * "Settings" subheader (AGL-3713).
30
54
  */ export const COMMERCE_CONSOLE_SECTIONS = [
31
55
  {
32
56
  id: 'catalog',
@@ -48,15 +72,66 @@
48
72
  id: 'reservations',
49
73
  label: 'Reservations'
50
74
  },
51
- {
52
- id: 'settings',
53
- label: 'Settings'
54
- },
55
75
  {
56
76
  id: 'analytics',
57
77
  label: 'Analytics'
78
+ },
79
+ {
80
+ id: 'payments',
81
+ label: 'Payments',
82
+ group: SETTINGS
83
+ },
84
+ {
85
+ id: 'checkout',
86
+ label: 'Checkout',
87
+ group: SETTINGS
88
+ },
89
+ {
90
+ id: 'taxes',
91
+ label: 'Taxes',
92
+ group: SETTINGS
93
+ },
94
+ {
95
+ id: 'shipping',
96
+ label: 'Shipping & delivery',
97
+ group: SETTINGS
98
+ },
99
+ {
100
+ id: 'notifications',
101
+ label: 'Notifications',
102
+ group: SETTINGS
103
+ },
104
+ {
105
+ id: 'pos',
106
+ label: 'Point of sale',
107
+ group: SETTINGS
108
+ },
109
+ // Nothing but other plugins' cards, so each is left out of the rail while
110
+ // none of them has anything to draw (`onlyWithZones`).
111
+ {
112
+ id: 'channels',
113
+ label: 'Sales channels',
114
+ group: SETTINGS,
115
+ onlyWithZones: [
116
+ COMMERCE_SETTINGS_PAGE_ZONES.channels
117
+ ]
118
+ },
119
+ {
120
+ id: 'integrations',
121
+ label: 'Integrations',
122
+ group: SETTINGS,
123
+ onlyWithZones: [
124
+ COMMERCE_SETTINGS_PAGE_ZONES.integrations
125
+ ]
58
126
  }
59
- ] /*
127
+ ];
128
+ /**
129
+ * Section ids the console no longer has, and where each one's links go
130
+ * (AGL-3713). `/products/settings` was every setting on one page; it opens
131
+ * Payments, the first of the pages it was split into.
132
+ */ export const COMMERCE_RETIRED_SECTIONS = {
133
+ settings: 'payments'
134
+ } /*
60
135
  * Rail ORDER decides where `/products` lands: the shell redirects a bare hub
61
136
  * URL to the first section in this list the reader may open (AGL-2501). There
62
137
  * is deliberately no separate default constant — a second place to say which
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/commerce-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type CommerceConsoleSectionId =\n | 'catalog'\n | 'orders'\n | 'returns'\n | 'promotions'\n | 'reservations'\n | 'settings'\n | 'analytics'\n\n/**\n * The commerce console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back. A second copy is how a section comes\n * to be routable under one id and drawn under another.\n *\n * Ids appear in links people keep — treat them as persisted, and rename a\n * LABEL rather than an id.\n *\n * No `navTabId` on any of them: every section here ships with the surface, so\n * they inherit the Products nav item's gate. A section that ships later\n * declares its own, which ANDs with the parent's rather than replacing it.\n */\nexport const COMMERCE_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'catalog', label: 'Catalog' },\n { id: 'orders', label: 'Orders' },\n { id: 'returns', label: 'Returns' },\n { id: 'promotions', label: 'Promotions' },\n { id: 'reservations', label: 'Reservations' },\n { id: 'settings', label: 'Settings' },\n { id: 'analytics', label: 'Analytics' },\n]\n\n/*\n * Rail ORDER decides where `/products` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["COMMERCE_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAaD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,4BAA0D;IACrE;QAAEC,IAAI;QAAWC,OAAO;IAAU;IAClC;QAAED,IAAI;QAAUC,OAAO;IAAS;IAChC;QAAED,IAAI;QAAWC,OAAO;IAAU;IAClC;QAAED,IAAI;QAAcC,OAAO;IAAa;IACxC;QAAED,IAAI;QAAgBC,OAAO;IAAe;IAC5C;QAAED,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAaC,OAAO;IAAY;CACvC,CAED;;;;;CAKC,IAPA"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/commerce-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type CommerceConsoleSectionId =\n | 'catalog'\n | 'orders'\n | 'returns'\n | 'promotions'\n | 'reservations'\n | 'analytics'\n | CommerceSettingsSectionId\n\n/**\n * The store's settings, one page each (AGL-3713). They were one section,\n * `settings`, and one long column: payments, taxes, shipping, the register\n * and every other plugin's cards, one after another.\n */\nexport type CommerceSettingsSectionId =\n | 'payments'\n | 'checkout'\n | 'taxes'\n | 'shipping'\n | 'notifications'\n | 'pos'\n | 'channels'\n | 'integrations'\n\n/** The words above the settings pages in the rail (AGL-3713). */\nexport const COMMERCE_SETTINGS_GROUP = 'Settings'\n\n/**\n * The zone each settings page draws at its foot, keyed by page (AGL-3713).\n *\n * One zone per page, so a plugin chooses its page by the zone it names and\n * nothing else: a contributor restates the id, never imports this package.\n * `commerceSettings` — the zone every store-settings widget named before the\n * split — is the Integrations page's, so a widget that names no page lands\n * there rather than nowhere.\n */\nexport const COMMERCE_SETTINGS_PAGE_ZONES: Readonly<\n Record<CommerceSettingsSectionId, string>\n> = {\n payments: 'commerceSettingsPayments',\n checkout: 'commerceSettingsCheckout',\n taxes: 'commerceSettingsTaxes',\n shipping: 'commerceSettingsShipping',\n notifications: 'commerceSettingsNotifications',\n pos: 'commerceSettingsPos',\n channels: 'commerceSettingsChannels',\n integrations: 'commerceSettings',\n}\n\n/** Under the \"Settings\" subheader (AGL-3713). */\nconst SETTINGS = COMMERCE_SETTINGS_GROUP\n\n/**\n * The commerce console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back. A second copy is how a section comes\n * to be routable under one id and drawn under another.\n *\n * Ids appear in links people keep — treat them as persisted, and rename a\n * LABEL rather than an id. An id that has to go is mapped in\n * {@link COMMERCE_RETIRED_SECTIONS} to the section that replaced it.\n *\n * No `navTabId` on any of them: every section here ships with the surface, so\n * they inherit the Products nav item's gate. A section that ships later\n * declares its own, which ANDs with the parent's rather than replacing it.\n *\n * The store's own work first, then its settings under a divider and a\n * \"Settings\" subheader (AGL-3713).\n */\nexport const COMMERCE_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'catalog', label: 'Catalog' },\n { id: 'orders', label: 'Orders' },\n { id: 'returns', label: 'Returns' },\n { id: 'promotions', label: 'Promotions' },\n { id: 'reservations', label: 'Reservations' },\n { id: 'analytics', label: 'Analytics' },\n { id: 'payments', label: 'Payments', group: SETTINGS },\n { id: 'checkout', label: 'Checkout', group: SETTINGS },\n { id: 'taxes', label: 'Taxes', group: SETTINGS },\n { id: 'shipping', label: 'Shipping & delivery', group: SETTINGS },\n { id: 'notifications', label: 'Notifications', group: SETTINGS },\n { id: 'pos', label: 'Point of sale', group: SETTINGS },\n // Nothing but other plugins' cards, so each is left out of the rail while\n // none of them has anything to draw (`onlyWithZones`).\n {\n id: 'channels',\n label: 'Sales channels',\n group: SETTINGS,\n onlyWithZones: [COMMERCE_SETTINGS_PAGE_ZONES.channels],\n },\n {\n id: 'integrations',\n label: 'Integrations',\n group: SETTINGS,\n onlyWithZones: [COMMERCE_SETTINGS_PAGE_ZONES.integrations],\n },\n]\n\n/**\n * Section ids the console no longer has, and where each one's links go\n * (AGL-3713). `/products/settings` was every setting on one page; it opens\n * Payments, the first of the pages it was split into.\n */\nexport const COMMERCE_RETIRED_SECTIONS: Readonly<Record<string, string>> = {\n settings: 'payments',\n}\n\n/*\n * Rail ORDER decides where `/products` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["COMMERCE_SETTINGS_GROUP","COMMERCE_SETTINGS_PAGE_ZONES","payments","checkout","taxes","shipping","notifications","pos","channels","integrations","SETTINGS","COMMERCE_CONSOLE_SECTIONS","id","label","group","onlyWithZones","COMMERCE_RETIRED_SECTIONS","settings"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA4BD,+DAA+D,GAC/D,OAAO,MAAMA,0BAA0B,WAAU;AAEjD;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAET;IACFC,UAAU;IACVC,UAAU;IACVC,OAAO;IACPC,UAAU;IACVC,eAAe;IACfC,KAAK;IACLC,UAAU;IACVC,cAAc;AAChB,EAAC;AAED,+CAA+C,GAC/C,MAAMC,WAAWV;AAEjB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,MAAMW,4BAA0D;IACrE;QAAEC,IAAI;QAAWC,OAAO;IAAU;IAClC;QAAED,IAAI;QAAUC,OAAO;IAAS;IAChC;QAAED,IAAI;QAAWC,OAAO;IAAU;IAClC;QAAED,IAAI;QAAcC,OAAO;IAAa;IACxC;QAAED,IAAI;QAAgBC,OAAO;IAAe;IAC5C;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAYC,OAAO;QAAYC,OAAOJ;IAAS;IACrD;QAAEE,IAAI;QAAYC,OAAO;QAAYC,OAAOJ;IAAS;IACrD;QAAEE,IAAI;QAASC,OAAO;QAASC,OAAOJ;IAAS;IAC/C;QAAEE,IAAI;QAAYC,OAAO;QAAuBC,OAAOJ;IAAS;IAChE;QAAEE,IAAI;QAAiBC,OAAO;QAAiBC,OAAOJ;IAAS;IAC/D;QAAEE,IAAI;QAAOC,OAAO;QAAiBC,OAAOJ;IAAS;IACrD,0EAA0E;IAC1E,uDAAuD;IACvD;QACEE,IAAI;QACJC,OAAO;QACPC,OAAOJ;QACPK,eAAe;YAACd,6BAA6BO,QAAQ;SAAC;IACxD;IACA;QACEI,IAAI;QACJC,OAAO;QACPC,OAAOJ;QACPK,eAAe;YAACd,6BAA6BQ,YAAY;SAAC;IAC5D;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,MAAMO,4BAA8D;IACzEC,UAAU;AACZ,EAEA;;;;;CAKC,IAPA"}
@@ -22,7 +22,7 @@ import * as CommerceModel from "../../model/index.js";
22
22
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
23
23
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
24
  import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
25
- import { collection, deleteDoc, doc, setDoc } from "firebase/firestore";
25
+ import { collection, deleteDoc, doc, setDoc, updateDoc } from "firebase/firestore";
26
26
  import { useCallback, useState } from "react";
27
27
  import { useFirestore } from "@aglyn/tenant-feature-instance";
28
28
  import { collectionPage, usePagedCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
@@ -138,6 +138,34 @@ import { COMMERCE_DISCOUNTS_TRANSFER } from "../../transfer/transfer-keys.js";
138
138
  discountsFromCache,
139
139
  discountsStatus
140
140
  ]);
141
+ /**
142
+ * Switches a discount on or off (AGL-3774). Imported and AI-proposed
143
+ * discounts start switched off, and Save keeps `enabled` as it was, so
144
+ * without this a disabled discount could never be turned back on here.
145
+ *
146
+ * Writes ONLY `enabled`: unlike Save it carries no copy of the seeded row,
147
+ * so it cannot roll back the live `redemptions` counter (AGL-1358).
148
+ */ const handleToggle = useCallback(async (discount)=>{
149
+ const enabled = discount.enabled === false;
150
+ try {
151
+ await updateDoc(doc(firestore, 'hosts', hostId, 'discounts', discount.$id), {
152
+ enabled
153
+ });
154
+ enqueueSnackbar(enabled ? 'Discount enabled' : 'Discount disabled', {
155
+ variant: 'success',
156
+ persist: false
157
+ });
158
+ } catch (unused) {
159
+ enqueueSnackbar('Could not change the discount. Try again.', {
160
+ variant: 'error',
161
+ persist: false
162
+ });
163
+ }
164
+ }, [
165
+ firestore,
166
+ hostId,
167
+ enqueueSnackbar
168
+ ]);
141
169
  const describe = (discount)=>{
142
170
  var _discount_valueCents;
143
171
  const value = discount.kind === 'percent' ? `${discount.valuePct}% off` : discount.kind === 'fixed' ? `$${(((_discount_valueCents = discount.valueCents) != null ? _discount_valueCents : 0) / 100).toFixed(2)} off` : 'Free shipping';
@@ -167,8 +195,8 @@ import { COMMERCE_DISCOUNTS_TRANSFER } from "../../transfer/transfer-keys.js";
167
195
  hostId: hostId
168
196
  })
169
197
  },
170
- help: pluginDocsHelp('commerce', {
171
- anchor: '#discounts'
198
+ help: pluginDocsHelp('discountsAndCoupons', {
199
+ anchor: '#add-a-discount'
172
200
  }),
173
201
  contentGutterX: true,
174
202
  contentGutterY: true,
@@ -204,6 +232,11 @@ import { COMMERCE_DISCOUNTS_TRANSFER } from "../../transfer/transfer-keys.js";
204
232
  discount.enabled === false ? ' · disabled' : ''
205
233
  ]
206
234
  }),
235
+ /*#__PURE__*/ _jsx(Button, {
236
+ size: "small",
237
+ onClick: ()=>void handleToggle(discount),
238
+ children: discount.enabled === false ? 'Enable' : 'Disable'
239
+ }),
207
240
  /*#__PURE__*/ _jsx(Button, {
208
241
  size: "small",
209
242
  onClick: ()=>setDraft(_extends({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/discounts-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_DISCOUNTS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface DiscountsCardProps {\n hostId: string\n}\n\n/**\n * Discounts hub (AGL-305): code and automatic promotions with value,\n * min-subtotal, schedule, and usage-limit controls, resolved at\n * checkout/POS by the shared engine. Supersedes the simple coupons\n * card (which keeps working for existing codes).\n */\nexport function DiscountsCard(props: DiscountsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n status: discountsStatus,\n /**\n * The row this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). `redemptions` is a LIVE counter the checkout engine\n * increments, and editing a discount copies it out of the seed and\n * writes it straight back — so a cached seed rolls the count backwards\n * and hands out redemptions that were already spent.\n */\n fromCache: discountsFromCache,\n rows: discounts,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(100)` alone is answered in DOCUMENT-ID order and the ids are\n * `createResourceUid()`, so the hundred were a pseudo-random sample of the\n * shop's promotions — and the hundred-and-first was UNREACHABLE, because\n * nothing rendered it and no control asked for more.\n *\n * The query can be paged here, unlike the cards carrying a duplicate-name\n * check: a discount's identity is its document id, `code` is an ordinary\n * field with no uniqueness test on this surface, and nothing on the card\n * is computed from rows that are off the page.\n *\n * `collectionPage` orders on the document NAME. `orderBy('code')` is the\n * tempting one and would be wrong twice: an AUTOMATIC promotion is stored\n * with `code: null`, so ordering on it would hide every automatic discount\n * the shop has, and a merchant reading \"no automatic promotions\" would be\n * reading a query artifact.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'discounts'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (Partial<CommerceModel.HostDiscount> & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n /**\n * `$id` comes off with `id` (AGL-1374).\n *\n * The editor is seeded `{ id: discount.$id, ...discount }`, and that\n * spread carries the listener's SYNTHETIC `$id` — `idField: '$id'`\n * stamps the document id onto the in-memory row, where nothing persists\n * it. Destructuring only `id` left `$id` in `data`, so every save wrote\n * it into the stored document as a real field. Nothing reads it; it is\n * the listener's bookkeeping leaking into storage, and once there no\n * reader can tell it from a field the card meant to write.\n */\n const seeded = draft as typeof draft & { $id?: string }\n const { id, $id: _syntheticId, ...data } = seeded\n const code = data.code?.trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '')\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * `merge: true` protects nothing here — `{...data}` carries every field\n * of the seeded row, `redemptions` included, so it is written back\n * whether or not anyone touched it. Under `persistentLocalCache` that\n * seed can be arbitrarily old, and a redemption cap silently regains\n * uses that buyers already spent.\n *\n * Only the edit path is guarded: a NEW discount is built from blanks at\n * a fresh uid and can overwrite nothing.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'discount',\n unreadable: Boolean(id) && discountsStatus === 'error',\n fromCache: Boolean(id) && discountsFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'discounts',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n ...(code ? { code } : { code: null }),\n enabled: data.enabled !== false,\n redemptions: data.redemptions ?? 0,\n },\n { merge: true },\n )\n },\n )\n // Keep the dialog open with what was typed rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Discount saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n discountsFromCache,\n discountsStatus,\n ])\n\n const describe = (discount: any) => {\n const value =\n discount.kind === 'percent'\n ? `${discount.valuePct}% off`\n : discount.kind === 'fixed'\n ? `$${((discount.valueCents ?? 0) / 100).toFixed(2)} off`\n : 'Free shipping'\n const scopeCount = Array.isArray(discount.productIds)\n ? discount.productIds.length\n : 0\n const extras = [\n // THE SCOPE, SAID OUT LOUD. A discount can cover named\n // products rather than the catalog, and the card showed no sign of it —\n // so a merchant could not tell a store-wide discount from a scoped one,\n // and had no way to understand why a scoped one takes less off than the\n // basket total. \"Store-wide\" is stated rather than left blank, because a\n // silent absence is what a narrower scope would also look like.\n scopeCount > 0\n ? `${scopeCount} product${scopeCount === 1 ? '' : 's'} only`\n : 'store-wide',\n discount.minSubtotalCents\n ? `min $${(discount.minSubtotalCents / 100).toFixed(0)}`\n : '',\n // Held slots are NAMED, not folded into the used figure (AGL-2453). A\n // live checkout now consumes a slot for as long as it can still be paid,\n // so `redemptions` alone would show a cap of 100 sitting at 100 against\n // 97 orders with nothing to explain the other three.\n discount.maxRedemptions\n ? CommerceModel.promotionUsageLabel(discount, Date.now())\n : '',\n ]\n .filter(Boolean)\n .join(' · ')\n return `${value}${extras ? ` · ${extras}` : ''}`\n }\n\n return (\n <CardDisplay\n header={'Discounts'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_DISCOUNTS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerce', { anchor: '#discounts' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {discounts.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Codes buyers enter at checkout, or automatic promotions that ' +\n 'apply on their own.'}\n </Typography>\n ) : (\n discounts.map((discount: any) => (\n <Stack\n key={discount.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Chip\n label={discount.code || discount.name || 'Automatic'}\n size=\"small\"\n variant=\"outlined\"\n color={discount.code ? 'primary' : 'info'}\n />\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {describe(discount)}\n {discount.enabled === false ? ' · disabled' : ''}\n </Typography>\n <Button\n size=\"small\"\n onClick={() => setDraft({ id: discount.$id, ...discount })}\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n deleteDoc(\n doc(firestore, 'hosts', hostId, 'discounts', discount.$id),\n )\n }\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n {discounts.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={discounts.length}\n // No `count`: a server window does not know the collection's\n // total, and `hasMore` is the probe row's answer rather than a\n // comparison against the cap.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, kind: 'percent', valuePct: 10, enabled: true })\n }\n >\n {'Add discount'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit discount' : 'New discount'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code (blank = automatic)\"\n value={draft?.code ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n placeholder=\"SAVE10\"\n />\n {!draft?.code ? (\n <TextField\n label=\"Promotion name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"Summer sale\"\n />\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Type\"\n value={draft?.kind ?? 'percent'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, kind: event.target.value as CommerceModel.DiscountKind }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ minWidth: 150 }}\n >\n <MenuItem value=\"percent\">{'Percent off'}</MenuItem>\n <MenuItem value=\"fixed\">{'Fixed amount off'}</MenuItem>\n <MenuItem value=\"free_shipping\">{'Free shipping'}</MenuItem>\n </TextField>\n {draft?.kind === 'percent' ? (\n <TextField\n label=\"%\"\n value={draft?.valuePct ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valuePct: Math.min(\n 100,\n Math.max(0, Number(event.target.value) || 0),\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 90 }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n ) : null}\n {draft?.kind === 'fixed' ? (\n <TextField\n label=\"$ off\"\n value={\n draft?.valueCents != null ? draft.valueCents / 100 : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valueCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 110 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Min subtotal ($)\"\n placeholder=\"None\"\n value={\n draft?.minSubtotalCents != null\n ? draft.minSubtotalCents / 100\n : ''\n }\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n minSubtotalCents:\n raw === ''\n ? undefined\n : Math.round((Number(raw) || 0) * 100),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Max uses\"\n placeholder=\"Unlimited\"\n value={draft?.maxRedemptions ?? ''}\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n maxRedemptions:\n raw === ''\n ? undefined\n : Math.max(1, Math.round(Number(raw))),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Starts\"\n type=\"date\"\n value={\n draft?.startAtMs\n ? new Date(draft.startAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n startAtMs: event.target.value\n ? Date.parse(`${event.target.value}T00:00:00Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n label=\"Ends\"\n type=\"date\"\n value={\n draft?.endAtMs\n ? new Date(draft.endAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n endAtMs: event.target.value\n ? Date.parse(`${event.target.value}T23:59:59Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n (draft?.kind === 'percent' && !((draft?.valuePct ?? 0) > 0)) ||\n (draft?.kind === 'fixed' && !((draft?.valueCents ?? 0) > 0))\n }\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nDiscountsCard.displayName = 'DiscountsCard'\n\nexport default DiscountsCard\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","pluginDocsHelp","TransferHeaderActions","COMMERCE_DISCOUNTS_TRANSFER","DiscountsCard","props","hostId","firestore","enqueueSnackbar","status","discountsStatus","fromCache","discountsFromCache","rows","discounts","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","data","seeded","id","$id","_syntheticId","code","trim","toUpperCase","replace","verdict","subject","unreadable","Boolean","createResourceUid","enabled","redemptions","merge","ok","message","variant","persist","describe","discount","value","kind","valuePct","valueCents","toFixed","scopeCount","Array","isArray","productIds","length","extras","minSubtotalCents","maxRedemptions","promotionUsageLabel","Date","now","filter","join","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","color","map","direction","sx","alignItems","label","name","size","flex","noWrap","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","onChange","event","prev","target","mt","placeholder","select","minWidth","Math","min","max","Number","width","slotProps","htmlInput","inputMode","round","raw","undefined","type","startAtMs","toISOString","slice","parse","inputLabel","shrink","endAtMs","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/discounts-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_DISCOUNTS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface DiscountsCardProps {\n hostId: string\n}\n\n/**\n * Discounts hub (AGL-305): code and automatic promotions with value,\n * min-subtotal, schedule, and usage-limit controls, resolved at\n * checkout/POS by the shared engine. Supersedes the simple coupons\n * card (which keeps working for existing codes).\n */\nexport function DiscountsCard(props: DiscountsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n status: discountsStatus,\n /**\n * The row this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). `redemptions` is a LIVE counter the checkout engine\n * increments, and editing a discount copies it out of the seed and\n * writes it straight back — so a cached seed rolls the count backwards\n * and hands out redemptions that were already spent.\n */\n fromCache: discountsFromCache,\n rows: discounts,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(100)` alone is answered in DOCUMENT-ID order and the ids are\n * `createResourceUid()`, so the hundred were a pseudo-random sample of the\n * shop's promotions — and the hundred-and-first was UNREACHABLE, because\n * nothing rendered it and no control asked for more.\n *\n * The query can be paged here, unlike the cards carrying a duplicate-name\n * check: a discount's identity is its document id, `code` is an ordinary\n * field with no uniqueness test on this surface, and nothing on the card\n * is computed from rows that are off the page.\n *\n * `collectionPage` orders on the document NAME. `orderBy('code')` is the\n * tempting one and would be wrong twice: an AUTOMATIC promotion is stored\n * with `code: null`, so ordering on it would hide every automatic discount\n * the shop has, and a merchant reading \"no automatic promotions\" would be\n * reading a query artifact.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'discounts'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (Partial<CommerceModel.HostDiscount> & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n /**\n * `$id` comes off with `id` (AGL-1374).\n *\n * The editor is seeded `{ id: discount.$id, ...discount }`, and that\n * spread carries the listener's SYNTHETIC `$id` — `idField: '$id'`\n * stamps the document id onto the in-memory row, where nothing persists\n * it. Destructuring only `id` left `$id` in `data`, so every save wrote\n * it into the stored document as a real field. Nothing reads it; it is\n * the listener's bookkeeping leaking into storage, and once there no\n * reader can tell it from a field the card meant to write.\n */\n const seeded = draft as typeof draft & { $id?: string }\n const { id, $id: _syntheticId, ...data } = seeded\n const code = data.code?.trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '')\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * `merge: true` protects nothing here — `{...data}` carries every field\n * of the seeded row, `redemptions` included, so it is written back\n * whether or not anyone touched it. Under `persistentLocalCache` that\n * seed can be arbitrarily old, and a redemption cap silently regains\n * uses that buyers already spent.\n *\n * Only the edit path is guarded: a NEW discount is built from blanks at\n * a fresh uid and can overwrite nothing.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'discount',\n unreadable: Boolean(id) && discountsStatus === 'error',\n fromCache: Boolean(id) && discountsFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'discounts',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n ...(code ? { code } : { code: null }),\n enabled: data.enabled !== false,\n redemptions: data.redemptions ?? 0,\n },\n { merge: true },\n )\n },\n )\n // Keep the dialog open with what was typed rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Discount saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n discountsFromCache,\n discountsStatus,\n ])\n\n /**\n * Switches a discount on or off (AGL-3774). Imported and AI-proposed\n * discounts start switched off, and Save keeps `enabled` as it was, so\n * without this a disabled discount could never be turned back on here.\n *\n * Writes ONLY `enabled`: unlike Save it carries no copy of the seeded row,\n * so it cannot roll back the live `redemptions` counter (AGL-1358).\n */\n const handleToggle = useCallback(\n async (discount: any) => {\n const enabled = discount.enabled === false\n try {\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'discounts', discount.$id),\n { enabled },\n )\n enqueueSnackbar(enabled ? 'Discount enabled' : 'Discount disabled', {\n variant: 'success',\n persist: false,\n })\n } catch {\n enqueueSnackbar('Could not change the discount. Try again.', {\n variant: 'error',\n persist: false,\n })\n }\n },\n [firestore, hostId, enqueueSnackbar],\n )\n\n const describe = (discount: any) => {\n const value =\n discount.kind === 'percent'\n ? `${discount.valuePct}% off`\n : discount.kind === 'fixed'\n ? `$${((discount.valueCents ?? 0) / 100).toFixed(2)} off`\n : 'Free shipping'\n const scopeCount = Array.isArray(discount.productIds)\n ? discount.productIds.length\n : 0\n const extras = [\n // THE SCOPE, SAID OUT LOUD. A discount can cover named\n // products rather than the catalog, and the card showed no sign of it —\n // so a merchant could not tell a store-wide discount from a scoped one,\n // and had no way to understand why a scoped one takes less off than the\n // basket total. \"Store-wide\" is stated rather than left blank, because a\n // silent absence is what a narrower scope would also look like.\n scopeCount > 0\n ? `${scopeCount} product${scopeCount === 1 ? '' : 's'} only`\n : 'store-wide',\n discount.minSubtotalCents\n ? `min $${(discount.minSubtotalCents / 100).toFixed(0)}`\n : '',\n // Held slots are NAMED, not folded into the used figure (AGL-2453). A\n // live checkout now consumes a slot for as long as it can still be paid,\n // so `redemptions` alone would show a cap of 100 sitting at 100 against\n // 97 orders with nothing to explain the other three.\n discount.maxRedemptions\n ? CommerceModel.promotionUsageLabel(discount, Date.now())\n : '',\n ]\n .filter(Boolean)\n .join(' · ')\n return `${value}${extras ? ` · ${extras}` : ''}`\n }\n\n return (\n <CardDisplay\n header={'Discounts'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_DISCOUNTS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('discountsAndCoupons', { anchor: '#add-a-discount' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {discounts.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Codes buyers enter at checkout, or automatic promotions that ' +\n 'apply on their own.'}\n </Typography>\n ) : (\n discounts.map((discount: any) => (\n <Stack\n key={discount.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Chip\n label={discount.code || discount.name || 'Automatic'}\n size=\"small\"\n variant=\"outlined\"\n color={discount.code ? 'primary' : 'info'}\n />\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {describe(discount)}\n {discount.enabled === false ? ' · disabled' : ''}\n </Typography>\n <Button size=\"small\" onClick={() => void handleToggle(discount)}>\n {discount.enabled === false ? 'Enable' : 'Disable'}\n </Button>\n <Button\n size=\"small\"\n onClick={() => setDraft({ id: discount.$id, ...discount })}\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n deleteDoc(\n doc(firestore, 'hosts', hostId, 'discounts', discount.$id),\n )\n }\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n {discounts.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={discounts.length}\n // No `count`: a server window does not know the collection's\n // total, and `hasMore` is the probe row's answer rather than a\n // comparison against the cap.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, kind: 'percent', valuePct: 10, enabled: true })\n }\n >\n {'Add discount'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit discount' : 'New discount'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code (blank = automatic)\"\n value={draft?.code ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n placeholder=\"SAVE10\"\n />\n {!draft?.code ? (\n <TextField\n label=\"Promotion name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"Summer sale\"\n />\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Type\"\n value={draft?.kind ?? 'percent'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, kind: event.target.value as CommerceModel.DiscountKind }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ minWidth: 150 }}\n >\n <MenuItem value=\"percent\">{'Percent off'}</MenuItem>\n <MenuItem value=\"fixed\">{'Fixed amount off'}</MenuItem>\n <MenuItem value=\"free_shipping\">{'Free shipping'}</MenuItem>\n </TextField>\n {draft?.kind === 'percent' ? (\n <TextField\n label=\"%\"\n value={draft?.valuePct ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valuePct: Math.min(\n 100,\n Math.max(0, Number(event.target.value) || 0),\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 90 }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n ) : null}\n {draft?.kind === 'fixed' ? (\n <TextField\n label=\"$ off\"\n value={\n draft?.valueCents != null ? draft.valueCents / 100 : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valueCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 110 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Min subtotal ($)\"\n placeholder=\"None\"\n value={\n draft?.minSubtotalCents != null\n ? draft.minSubtotalCents / 100\n : ''\n }\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n minSubtotalCents:\n raw === ''\n ? undefined\n : Math.round((Number(raw) || 0) * 100),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Max uses\"\n placeholder=\"Unlimited\"\n value={draft?.maxRedemptions ?? ''}\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n maxRedemptions:\n raw === ''\n ? undefined\n : Math.max(1, Math.round(Number(raw))),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Starts\"\n type=\"date\"\n value={\n draft?.startAtMs\n ? new Date(draft.startAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n startAtMs: event.target.value\n ? Date.parse(`${event.target.value}T00:00:00Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n label=\"Ends\"\n type=\"date\"\n value={\n draft?.endAtMs\n ? new Date(draft.endAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n endAtMs: event.target.value\n ? Date.parse(`${event.target.value}T23:59:59Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n (draft?.kind === 'percent' && !((draft?.valuePct ?? 0) > 0)) ||\n (draft?.kind === 'fixed' && !((draft?.valueCents ?? 0) > 0))\n }\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nDiscountsCard.displayName = 'DiscountsCard'\n\nexport default DiscountsCard\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","updateDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","pluginDocsHelp","TransferHeaderActions","COMMERCE_DISCOUNTS_TRANSFER","DiscountsCard","props","hostId","firestore","enqueueSnackbar","status","discountsStatus","fromCache","discountsFromCache","rows","discounts","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","data","seeded","id","$id","_syntheticId","code","trim","toUpperCase","replace","verdict","subject","unreadable","Boolean","createResourceUid","enabled","redemptions","merge","ok","message","variant","persist","handleToggle","discount","describe","value","kind","valuePct","valueCents","toFixed","scopeCount","Array","isArray","productIds","length","extras","minSubtotalCents","maxRedemptions","promotionUsageLabel","Date","now","filter","join","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","color","map","direction","sx","alignItems","label","name","size","flex","noWrap","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","onChange","event","prev","target","mt","placeholder","select","minWidth","Math","min","max","Number","width","slotProps","htmlInput","inputMode","round","raw","undefined","type","startAtMs","toISOString","slice","parse","inputLabel","shrink","endAtMs","disabled","displayName"],"mappings":"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"}
@@ -105,7 +105,7 @@ import { COMMERCE_COUPONS_TRANSFER } from "../../transfer/transfer-keys.js";
105
105
  hostId: hostId
106
106
  })
107
107
  },
108
- help: pluginDocsHelp('commerce', {
108
+ help: pluginDocsHelp('discountsAndCoupons', {
109
109
  anchor: '#coupons'
110
110
  }),
111
111
  contentGutterX: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-coupons-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, query, setDoc, updateDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_COUPONS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface HostCouponsCardProps {\n hostId: string\n}\n\n/**\n * Coupon codes (AGL-96), extracted from the retired Commerce Starter\n * card when the products hub landed (AGL-279). Percent-off codes at\n * `hosts/{hostId}/coupons/{CODE}`; the discounts engine (AGL-305)\n * supersedes these.\n */\nexport function HostCouponsCard(props: HostCouponsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n /*\n * A growing window rather than a hard hundred, which a shop with more\n * codes than that could not see past.\n *\n * No `orderBy`: a coupon's document ID is its CODE, and document-ID order\n * is what Firestore returns by default — so the window is already the\n * alphabetical front of the list, which is what the sort below wants. The\n * sort is kept because it makes that dependency explicit rather than\n * relying on the default holding.\n */\n const {\n rows: couponDocs,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(collection(firestore, 'hosts', hostId, 'coupons'), limit(pageLimit)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const coupons = [...couponDocs].sort((a: any, b: any) =>\n String(a.$id).localeCompare(String(b.$id)),\n )\n const [couponDraft, setCouponDraft] = useState<{\n code: string\n percentOff: string\n maxRedemptions: string\n /** `YYYY-MM-DD` from a native date input, or '' for no expiry. */\n expiresOn: string\n } | null>(null)\n\n const handleCouponSave = useCallback(async () => {\n if (!couponDraft) return\n const code = couponDraft.code\n .trim()\n .toUpperCase()\n .replace(/[^A-Z0-9_-]/g, '')\n .slice(0, 40)\n const percentOff = Math.round(Number(couponDraft.percentOff))\n if (!code || !(percentOff > 0 && percentOff <= 100)) return\n // An unparseable value is dropped rather than written: a NaN here would\n // be a coupon that never expires or one that is born dead, depending on\n // how the server coerced it, and neither is what anyone typed.\n const expiresOn = couponDraft.expiresOn.trim()\n const parsedExpiry = expiresOn\n ? Date.parse(`${expiresOn}T23:59:59.999`)\n : NaN\n const expiresAtMs = Number.isFinite(parsedExpiry) ? parsedExpiry : null\n await setDoc(doc(firestore, 'hosts', hostId, 'coupons', code), {\n percentOff,\n enabled: true,\n redemptions: 0,\n ...(couponDraft.maxRedemptions.trim()\n ? {\n maxRedemptions: Math.max(\n 1,\n Math.round(Number(couponDraft.maxRedemptions)),\n ),\n }\n : {}),\n // THE FIELD THE CHECKOUTS ALREADY GATE ON.\n //\n // `checkout.ts` and `cart-checkout.ts` both refuse a coupon whose\n // `expiresAtMs` has passed, and nothing wrote it — so the gate read as\n // enforcement while every coupon ever created was immortal. A launch\n // code stays redeemable forever, which is a discount the merchant\n // believed they had withdrawn.\n //\n // The END of the chosen day, in the merchant's own timezone: \"expires\n // September 5th\" has to mean the 5th still works. The comparison on the\n // server is absolute epoch, so the local end-of-day is what carries that\n // meaning across.\n ...(expiresAtMs != null ? { expiresAtMs } : {}),\n createdAt: Timestamp.now(),\n })\n setCouponDraft(null)\n enqueueSnackbar(`Coupon ${code} created`, {\n variant: 'success',\n persist: false,\n })\n }, [couponDraft, firestore, hostId, enqueueSnackbar])\n\n const handleCouponToggle = useCallback(\n (coupon: any) => () =>\n updateDoc(doc(firestore, 'hosts', hostId, 'coupons', coupon.$id), {\n enabled: coupon.enabled === false,\n }),\n [firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Coupons'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_COUPONS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerce', { anchor: '#coupons' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {coupons.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Percent-off codes buyers can enter at checkout.'}\n </Typography>\n ) : (\n coupons.map((coupon: any) => (\n <Stack\n key={coupon.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {`${coupon.$id} · ${coupon.percentOff}% off · ` +\n // Held slots are named separately (AGL-2453) — see the\n // discounts card for why the used figure alone now misleads.\n CommerceModel.promotionUsageLabel(coupon, Date.now()) +\n // An expiry nobody can see is one the merchant cannot act\n // on, and an EXPIRED coupon still listed as live is the\n // question this card exists to answer.\n (coupon.expiresAtMs != null\n ? Number(coupon.expiresAtMs) < Date.now()\n ? ' · expired'\n : ` · expires ${new Date(\n Number(coupon.expiresAtMs),\n ).toLocaleDateString()}`\n : '') +\n (coupon.enabled === false ? ' · disabled' : '')}\n </Typography>\n <Button size=\"small\" onClick={handleCouponToggle(coupon)}>\n {coupon.enabled === false ? 'Enable' : 'Disable'}\n </Button>\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={coupons.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setCouponDraft({\n code: '',\n percentOff: '',\n maxRedemptions: '',\n expiresOn: '',\n })\n }\n >\n {'Add coupon'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(couponDraft)}\n onClose={() => setCouponDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'New coupon'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code\"\n value={couponDraft?.code ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n helperText=\"Letters/numbers; stored uppercase, e.g. LAUNCH20\"\n />\n <TextField\n label=\"Percent off\"\n type=\"number\"\n value={couponDraft?.percentOff ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, percentOff: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Max redemptions\"\n type=\"number\"\n placeholder=\"Blank = unlimited\"\n value={couponDraft?.maxRedemptions ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, maxRedemptions: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Expires\"\n type=\"date\"\n value={couponDraft?.expiresOn ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, expiresOn: event.target.value } : prev,\n )\n }\n size=\"small\"\n slotProps={{ inputLabel: { shrink: true } }}\n helperText=\"Blank = never expires. The code works through this day.\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setCouponDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !couponDraft?.code.trim() ||\n !(Number(couponDraft?.percentOff) > 0) ||\n Number(couponDraft?.percentOff) > 100\n }\n onClick={handleCouponSave}\n >\n {'Create'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostCouponsCard.displayName = 'HostCouponsCard'\n\nexport default HostCouponsCard\n"],"names":["CommerceModel","CardDisplay","ListPagination","useSnackbar","Timestamp","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","doc","limit","query","setDoc","updateDoc","useCallback","useState","useFirestore","usePagedCollection","pluginDocsHelp","TransferHeaderActions","COMMERCE_COUPONS_TRANSFER","HostCouponsCard","props","hostId","firestore","enqueueSnackbar","rows","couponDocs","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","coupons","sort","a","b","String","$id","localeCompare","couponDraft","setCouponDraft","handleCouponSave","code","trim","toUpperCase","replace","slice","percentOff","Math","round","Number","expiresOn","parsedExpiry","Date","parse","NaN","expiresAtMs","isFinite","enabled","redemptions","maxRedemptions","max","createdAt","now","variant","persist","handleCouponToggle","coupon","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","length","color","map","direction","sx","alignItems","flex","noWrap","promotionUsageLabel","toLocaleDateString","size","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","type","placeholder","slotProps","inputLabel","shrink","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-coupons-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, query, setDoc, updateDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_COUPONS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface HostCouponsCardProps {\n hostId: string\n}\n\n/**\n * Coupon codes (AGL-96), extracted from the retired Commerce Starter\n * card when the products hub landed (AGL-279). Percent-off codes at\n * `hosts/{hostId}/coupons/{CODE}`; the discounts engine (AGL-305)\n * supersedes these.\n */\nexport function HostCouponsCard(props: HostCouponsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n /*\n * A growing window rather than a hard hundred, which a shop with more\n * codes than that could not see past.\n *\n * No `orderBy`: a coupon's document ID is its CODE, and document-ID order\n * is what Firestore returns by default — so the window is already the\n * alphabetical front of the list, which is what the sort below wants. The\n * sort is kept because it makes that dependency explicit rather than\n * relying on the default holding.\n */\n const {\n rows: couponDocs,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(collection(firestore, 'hosts', hostId, 'coupons'), limit(pageLimit)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const coupons = [...couponDocs].sort((a: any, b: any) =>\n String(a.$id).localeCompare(String(b.$id)),\n )\n const [couponDraft, setCouponDraft] = useState<{\n code: string\n percentOff: string\n maxRedemptions: string\n /** `YYYY-MM-DD` from a native date input, or '' for no expiry. */\n expiresOn: string\n } | null>(null)\n\n const handleCouponSave = useCallback(async () => {\n if (!couponDraft) return\n const code = couponDraft.code\n .trim()\n .toUpperCase()\n .replace(/[^A-Z0-9_-]/g, '')\n .slice(0, 40)\n const percentOff = Math.round(Number(couponDraft.percentOff))\n if (!code || !(percentOff > 0 && percentOff <= 100)) return\n // An unparseable value is dropped rather than written: a NaN here would\n // be a coupon that never expires or one that is born dead, depending on\n // how the server coerced it, and neither is what anyone typed.\n const expiresOn = couponDraft.expiresOn.trim()\n const parsedExpiry = expiresOn\n ? Date.parse(`${expiresOn}T23:59:59.999`)\n : NaN\n const expiresAtMs = Number.isFinite(parsedExpiry) ? parsedExpiry : null\n await setDoc(doc(firestore, 'hosts', hostId, 'coupons', code), {\n percentOff,\n enabled: true,\n redemptions: 0,\n ...(couponDraft.maxRedemptions.trim()\n ? {\n maxRedemptions: Math.max(\n 1,\n Math.round(Number(couponDraft.maxRedemptions)),\n ),\n }\n : {}),\n // THE FIELD THE CHECKOUTS ALREADY GATE ON.\n //\n // `checkout.ts` and `cart-checkout.ts` both refuse a coupon whose\n // `expiresAtMs` has passed, and nothing wrote it — so the gate read as\n // enforcement while every coupon ever created was immortal. A launch\n // code stays redeemable forever, which is a discount the merchant\n // believed they had withdrawn.\n //\n // The END of the chosen day, in the merchant's own timezone: \"expires\n // September 5th\" has to mean the 5th still works. The comparison on the\n // server is absolute epoch, so the local end-of-day is what carries that\n // meaning across.\n ...(expiresAtMs != null ? { expiresAtMs } : {}),\n createdAt: Timestamp.now(),\n })\n setCouponDraft(null)\n enqueueSnackbar(`Coupon ${code} created`, {\n variant: 'success',\n persist: false,\n })\n }, [couponDraft, firestore, hostId, enqueueSnackbar])\n\n const handleCouponToggle = useCallback(\n (coupon: any) => () =>\n updateDoc(doc(firestore, 'hosts', hostId, 'coupons', coupon.$id), {\n enabled: coupon.enabled === false,\n }),\n [firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Coupons'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_COUPONS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('discountsAndCoupons', { anchor: '#coupons' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {coupons.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Percent-off codes buyers can enter at checkout.'}\n </Typography>\n ) : (\n coupons.map((coupon: any) => (\n <Stack\n key={coupon.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {`${coupon.$id} · ${coupon.percentOff}% off · ` +\n // Held slots are named separately (AGL-2453) — see the\n // discounts card for why the used figure alone now misleads.\n CommerceModel.promotionUsageLabel(coupon, Date.now()) +\n // An expiry nobody can see is one the merchant cannot act\n // on, and an EXPIRED coupon still listed as live is the\n // question this card exists to answer.\n (coupon.expiresAtMs != null\n ? Number(coupon.expiresAtMs) < Date.now()\n ? ' · expired'\n : ` · expires ${new Date(\n Number(coupon.expiresAtMs),\n ).toLocaleDateString()}`\n : '') +\n (coupon.enabled === false ? ' · disabled' : '')}\n </Typography>\n <Button size=\"small\" onClick={handleCouponToggle(coupon)}>\n {coupon.enabled === false ? 'Enable' : 'Disable'}\n </Button>\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={coupons.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setCouponDraft({\n code: '',\n percentOff: '',\n maxRedemptions: '',\n expiresOn: '',\n })\n }\n >\n {'Add coupon'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(couponDraft)}\n onClose={() => setCouponDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'New coupon'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code\"\n value={couponDraft?.code ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n helperText=\"Letters/numbers; stored uppercase, e.g. LAUNCH20\"\n />\n <TextField\n label=\"Percent off\"\n type=\"number\"\n value={couponDraft?.percentOff ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, percentOff: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Max redemptions\"\n type=\"number\"\n placeholder=\"Blank = unlimited\"\n value={couponDraft?.maxRedemptions ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, maxRedemptions: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Expires\"\n type=\"date\"\n value={couponDraft?.expiresOn ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, expiresOn: event.target.value } : prev,\n )\n }\n size=\"small\"\n slotProps={{ inputLabel: { shrink: true } }}\n helperText=\"Blank = never expires. The code works through this day.\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setCouponDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !couponDraft?.code.trim() ||\n !(Number(couponDraft?.percentOff) > 0) ||\n Number(couponDraft?.percentOff) > 100\n }\n onClick={handleCouponSave}\n >\n {'Create'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostCouponsCard.displayName = 'HostCouponsCard'\n\nexport default HostCouponsCard\n"],"names":["CommerceModel","CardDisplay","ListPagination","useSnackbar","Timestamp","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","doc","limit","query","setDoc","updateDoc","useCallback","useState","useFirestore","usePagedCollection","pluginDocsHelp","TransferHeaderActions","COMMERCE_COUPONS_TRANSFER","HostCouponsCard","props","hostId","firestore","enqueueSnackbar","rows","couponDocs","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","coupons","sort","a","b","String","$id","localeCompare","couponDraft","setCouponDraft","handleCouponSave","code","trim","toUpperCase","replace","slice","percentOff","Math","round","Number","expiresOn","parsedExpiry","Date","parse","NaN","expiresAtMs","isFinite","enabled","redemptions","maxRedemptions","max","createdAt","now","variant","persist","handleCouponToggle","coupon","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","length","color","map","direction","sx","alignItems","flex","noWrap","promotionUsageLabel","toLocaleDateString","size","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","type","placeholder","slotProps","inputLabel","shrink","disabled","displayName"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { type ReactNode } from 'react';
2
+ import * as CommerceModel from '../../../model';
3
+ import { posDisplayCall } from './pos-api';
4
+ type User = Parameters<typeof posDisplayCall>[0];
5
+ /** A code on screen, and the register and kind of device it pairs. */
6
+ export interface PosPairingState {
7
+ registerId: string;
8
+ registerName?: string;
9
+ mode: CommerceModel.PosDeviceMode;
10
+ code: string;
11
+ expiresAtMs: number;
12
+ }
13
+ /**
14
+ * A fresh six-digit code that pairs a tablet to one register, as a customer
15
+ * display or a self-service kiosk (AGL-3608, AGL-3623). The server holds the
16
+ * gate: the register's role and `managePos` checks and the plan's `pos`.
17
+ */
18
+ export declare function requestPosPairingCode(user: User, hostId: string, registerId: string, mode?: CommerceModel.PosDeviceMode): Promise<{
19
+ code: string;
20
+ expiresAtMs: number;
21
+ }>;
22
+ /** The address a tablet opens to enter the code. */
23
+ export declare function posPairingAddress(mode: CommerceModel.PosDeviceMode): string;
24
+ export interface PosPairingDialogProps {
25
+ pairing: PosPairingState | null;
26
+ onModeChange: (mode: CommerceModel.PosDeviceMode) => void;
27
+ onClose: () => void;
28
+ }
29
+ /** The code, the address to enter it at, and the display-or-kiosk choice. */
30
+ export declare function PosPairingDialog({ pairing, onModeChange, onClose }: PosPairingDialogProps): import("react").JSX.Element;
31
+ export declare namespace PosPairingDialog {
32
+ var displayName: string;
33
+ }
34
+ export interface UsePosPairingOptions {
35
+ user: User | null | undefined;
36
+ hostId: string;
37
+ /** Told the reason a code could not be made. */
38
+ onError: (message: string) => void;
39
+ }
40
+ /**
41
+ * The one pairing flow, for the register and the POS devices card alike:
42
+ * `open(registerId)` asks for a code and shows it; switching between
43
+ * customer display and kiosk asks for a code of that kind. Render `dialog`.
44
+ */
45
+ export declare function usePosPairing({ user, hostId, onError }: UsePosPairingOptions): {
46
+ pairing: PosPairingState | null;
47
+ open: (registerId: string, mode?: CommerceModel.PosDeviceMode, registerName?: string) => Promise<void>;
48
+ close: () => void;
49
+ dialog: ReactNode;
50
+ };
51
+ /** The element id the POS registers card carries, for a link that lands on it. */
52
+ export declare const POS_REGISTERS_ANCHOR = "pos-registers";
53
+ /**
54
+ * Where a register is created — the POS registers card, on Products → Point
55
+ * of sale (`…/products/pos`, AGL-3713) — from the register page's own console
56
+ * path (`…/pos`). Undefined when that path is not the register's.
57
+ */
58
+ export declare function posRegistersSettingsHref(posBasePath: string | undefined): string | undefined;
59
+ export {};