@semics-tech/emdash-vouchers-pro 0.0.0-bootstrap.0 → 1.0.0-rc.12

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.
@@ -0,0 +1,5 @@
1
+ // Auto-generated by emdash-plugin build. Do not edit.
2
+ import type { PluginDescriptor } from "emdash";
3
+
4
+ declare const descriptor: PluginDescriptor;
5
+ export default descriptor;
package/dist/index.mjs ADDED
@@ -0,0 +1,264 @@
1
+ // Auto-generated by emdash-plugin build. Do not edit.
2
+ // Source: emdash-plugin.jsonc + package.json
3
+ //
4
+ // Default-exports a sandboxed plugin descriptor. Pass it directly into
5
+ // emdash's `plugins:` or `sandboxed:` array — no factory call needed.
6
+
7
+ /** @type {import("emdash").PluginDescriptor} */
8
+ const descriptor = Object.freeze({
9
+ "id": "semics-vouchers",
10
+ "version": "1.0.0-rc.12",
11
+ "format": "standard",
12
+ "entrypoint": "@semics-tech/emdash-vouchers-pro/sandbox",
13
+ "capabilities": [
14
+ "network:request",
15
+ "email:send"
16
+ ],
17
+ "allowedHosts": [
18
+ "api.stripe.com",
19
+ "challenges.cloudflare.com"
20
+ ],
21
+ "storage": {
22
+ "vouchers": {
23
+ "indexes": [
24
+ "status",
25
+ "kind",
26
+ "expiresAt",
27
+ "last4",
28
+ "orderId",
29
+ "buyerEmail",
30
+ "recipientEmail",
31
+ "delivery",
32
+ [
33
+ "status",
34
+ "expiresAt"
35
+ ]
36
+ ]
37
+ },
38
+ "orders": {
39
+ "indexes": [
40
+ "status",
41
+ "createdAt",
42
+ "sessionId"
43
+ ]
44
+ },
45
+ "redemptions": {
46
+ "indexes": [
47
+ "voucherId",
48
+ "ref",
49
+ "state",
50
+ "lateOpen",
51
+ [
52
+ "state",
53
+ "createdAt"
54
+ ]
55
+ ]
56
+ },
57
+ "clients": {
58
+ "indexes": [
59
+ "createdAt"
60
+ ]
61
+ },
62
+ "outbox": {
63
+ "indexes": [
64
+ [
65
+ "status",
66
+ "nextAttemptAt"
67
+ ]
68
+ ]
69
+ }
70
+ },
71
+ "hooks": [
72
+ "plugin:activate",
73
+ "cron"
74
+ ],
75
+ "routes": [
76
+ {
77
+ "name": "public/purchase",
78
+ "methods": [
79
+ "POST"
80
+ ],
81
+ "public": true
82
+ },
83
+ {
84
+ "name": "public/redeem/hold",
85
+ "methods": [
86
+ "POST"
87
+ ],
88
+ "public": true
89
+ },
90
+ {
91
+ "name": "public/redeem/settle",
92
+ "methods": [
93
+ "POST"
94
+ ],
95
+ "public": true
96
+ },
97
+ {
98
+ "name": "public/redeem/check",
99
+ "methods": [
100
+ "POST"
101
+ ],
102
+ "public": true
103
+ },
104
+ {
105
+ "name": "public/options",
106
+ "methods": [
107
+ "POST"
108
+ ],
109
+ "public": true
110
+ },
111
+ {
112
+ "name": "webhooks/stripe",
113
+ "methods": [
114
+ "POST"
115
+ ],
116
+ "request": {
117
+ "body": "text",
118
+ "headers": [
119
+ "stripe-signature"
120
+ ]
121
+ },
122
+ "response": "raw",
123
+ "public": true
124
+ },
125
+ "pairing/create",
126
+ "pairing/revoke",
127
+ "admin",
128
+ {
129
+ "name": "reports/export",
130
+ "methods": [
131
+ "GET"
132
+ ],
133
+ "response": "raw"
134
+ },
135
+ "pro/licence"
136
+ ],
137
+ "adminPages": [
138
+ {
139
+ "path": "/vouchers",
140
+ "label": "Vouchers",
141
+ "icon": "ticket"
142
+ },
143
+ {
144
+ "path": "/to-post",
145
+ "label": "To post",
146
+ "icon": "mail"
147
+ },
148
+ {
149
+ "path": "/unsettled",
150
+ "label": "Unsettled",
151
+ "icon": "calendar"
152
+ },
153
+ {
154
+ "path": "/reports",
155
+ "label": "Voucher reports",
156
+ "icon": "chart"
157
+ },
158
+ {
159
+ "path": "/issue",
160
+ "label": "Issue a voucher",
161
+ "icon": "tag"
162
+ },
163
+ {
164
+ "path": "/pairing",
165
+ "label": "Pairing",
166
+ "icon": "gear"
167
+ }
168
+ ],
169
+ "settingsSchema": {
170
+ "kinds": {
171
+ "label": "Kinds of voucher sold",
172
+ "description": "money, activity, or both (comma-separated).",
173
+ "type": "string",
174
+ "default": "money,activity"
175
+ },
176
+ "validityMonths": {
177
+ "label": "Valid for (months)",
178
+ "description": "Applies to vouchers sold from now on. Existing vouchers keep their own expiry.",
179
+ "type": "number",
180
+ "default": 12,
181
+ "min": 1,
182
+ "max": 60
183
+ },
184
+ "moneyAmounts": {
185
+ "label": "Preset amounts (pence)",
186
+ "description": "Comma-separated, e.g. 2500,5000,10000 for £25, £50 and £100.",
187
+ "type": "string",
188
+ "default": "2500,5000,10000"
189
+ },
190
+ "moneyCustom": {
191
+ "label": "Buyers may choose another amount",
192
+ "type": "boolean",
193
+ "default": true
194
+ },
195
+ "moneyCustomMin": {
196
+ "label": "Smallest chosen amount (pence)",
197
+ "type": "number",
198
+ "default": 1000,
199
+ "min": 100,
200
+ "max": 10000000
201
+ },
202
+ "moneyCustomMax": {
203
+ "label": "Largest chosen amount (pence)",
204
+ "type": "number",
205
+ "default": 50000,
206
+ "min": 100,
207
+ "max": 10000000
208
+ },
209
+ "activityTypes": {
210
+ "label": "Activity vouchers",
211
+ "description": "One per line: event id, ticket type id, price in pence, label. For example: ev_123, tt_456, 3500, One clay session.",
212
+ "type": "string",
213
+ "default": "",
214
+ "multiline": true
215
+ },
216
+ "delivery": {
217
+ "label": "Delivery options",
218
+ "description": "Any of email-buyer, email-recipient, print, post (comma-separated).",
219
+ "type": "string",
220
+ "default": "email-buyer,email-recipient,print"
221
+ },
222
+ "scheduledSend": {
223
+ "label": "Buyers may pick the day the recipient's email goes out",
224
+ "type": "boolean",
225
+ "default": true
226
+ },
227
+ "currency": {
228
+ "label": "Currency",
229
+ "description": "ISO code, e.g. GBP. One currency per site.",
230
+ "type": "string",
231
+ "default": "GBP"
232
+ },
233
+ "turnstileSiteKey": {
234
+ "label": "Turnstile site key",
235
+ "type": "string"
236
+ },
237
+ "turnstileSecretKey": {
238
+ "label": "Turnstile secret key",
239
+ "type": "secret"
240
+ },
241
+ "stripeSecretKey": {
242
+ "label": "Stripe secret key",
243
+ "description": "Ideally a restricted key with write access to Checkout Sessions and Refunds. Stored encrypted.",
244
+ "type": "secret"
245
+ },
246
+ "stripeWebhookSecret": {
247
+ "label": "Stripe webhook signing secret",
248
+ "description": "The whsec_ secret of the site's Stripe webhook endpoint. Vouchers are issued only through it. Stored encrypted.",
249
+ "type": "secret"
250
+ },
251
+ "pairingMasterKey": {
252
+ "label": "Pairing master key",
253
+ "description": "Events pairing secrets are derived from this key. Generated when you pair Events; changing it un-pairs Events.",
254
+ "type": "secret"
255
+ },
256
+ "licenceKey": {
257
+ "label": "Pro licence key",
258
+ "description": "Your Events Pro (or Suite) semics1. key. Not needed on local or preview hosts. Stored encrypted.",
259
+ "type": "secret"
260
+ }
261
+ }
262
+ });
263
+
264
+ export default descriptor;
@@ -0,0 +1,269 @@
1
+ {
2
+ "id": "semics-vouchers",
3
+ "version": "1.0.0-rc.12",
4
+ "declaredAccess": {
5
+ "network": {
6
+ "request": {
7
+ "allowedHosts": [
8
+ "api.stripe.com",
9
+ "challenges.cloudflare.com"
10
+ ]
11
+ }
12
+ },
13
+ "email": {
14
+ "send": {}
15
+ }
16
+ },
17
+ "capabilities": [
18
+ "network:request",
19
+ "email:send"
20
+ ],
21
+ "allowedHosts": [
22
+ "api.stripe.com",
23
+ "challenges.cloudflare.com"
24
+ ],
25
+ "storage": {
26
+ "vouchers": {
27
+ "indexes": [
28
+ "status",
29
+ "kind",
30
+ "expiresAt",
31
+ "last4",
32
+ "orderId",
33
+ "buyerEmail",
34
+ "recipientEmail",
35
+ "delivery",
36
+ [
37
+ "status",
38
+ "expiresAt"
39
+ ]
40
+ ]
41
+ },
42
+ "orders": {
43
+ "indexes": [
44
+ "status",
45
+ "createdAt",
46
+ "sessionId"
47
+ ]
48
+ },
49
+ "redemptions": {
50
+ "indexes": [
51
+ "voucherId",
52
+ "ref",
53
+ "state",
54
+ "lateOpen",
55
+ [
56
+ "state",
57
+ "createdAt"
58
+ ]
59
+ ]
60
+ },
61
+ "clients": {
62
+ "indexes": [
63
+ "createdAt"
64
+ ]
65
+ },
66
+ "outbox": {
67
+ "indexes": [
68
+ [
69
+ "status",
70
+ "nextAttemptAt"
71
+ ]
72
+ ]
73
+ }
74
+ },
75
+ "hooks": [
76
+ "plugin:activate",
77
+ "cron"
78
+ ],
79
+ "routes": [
80
+ {
81
+ "name": "public/purchase",
82
+ "methods": [
83
+ "POST"
84
+ ],
85
+ "public": true
86
+ },
87
+ {
88
+ "name": "public/redeem/hold",
89
+ "methods": [
90
+ "POST"
91
+ ],
92
+ "public": true
93
+ },
94
+ {
95
+ "name": "public/redeem/settle",
96
+ "methods": [
97
+ "POST"
98
+ ],
99
+ "public": true
100
+ },
101
+ {
102
+ "name": "public/redeem/check",
103
+ "methods": [
104
+ "POST"
105
+ ],
106
+ "public": true
107
+ },
108
+ {
109
+ "name": "public/options",
110
+ "methods": [
111
+ "POST"
112
+ ],
113
+ "public": true
114
+ },
115
+ {
116
+ "name": "webhooks/stripe",
117
+ "methods": [
118
+ "POST"
119
+ ],
120
+ "request": {
121
+ "body": "text",
122
+ "headers": [
123
+ "stripe-signature"
124
+ ]
125
+ },
126
+ "response": "raw",
127
+ "public": true
128
+ },
129
+ "pairing/create",
130
+ "pairing/revoke",
131
+ "admin",
132
+ {
133
+ "name": "reports/export",
134
+ "methods": [
135
+ "GET"
136
+ ],
137
+ "response": "raw"
138
+ },
139
+ "pro/licence"
140
+ ],
141
+ "admin": {
142
+ "settingsSchema": {
143
+ "kinds": {
144
+ "label": "Kinds of voucher sold",
145
+ "description": "money, activity, or both (comma-separated).",
146
+ "type": "string",
147
+ "default": "money,activity"
148
+ },
149
+ "validityMonths": {
150
+ "label": "Valid for (months)",
151
+ "description": "Applies to vouchers sold from now on. Existing vouchers keep their own expiry.",
152
+ "type": "number",
153
+ "default": 12,
154
+ "min": 1,
155
+ "max": 60
156
+ },
157
+ "moneyAmounts": {
158
+ "label": "Preset amounts (pence)",
159
+ "description": "Comma-separated, e.g. 2500,5000,10000 for £25, £50 and £100.",
160
+ "type": "string",
161
+ "default": "2500,5000,10000"
162
+ },
163
+ "moneyCustom": {
164
+ "label": "Buyers may choose another amount",
165
+ "type": "boolean",
166
+ "default": true
167
+ },
168
+ "moneyCustomMin": {
169
+ "label": "Smallest chosen amount (pence)",
170
+ "type": "number",
171
+ "default": 1000,
172
+ "min": 100,
173
+ "max": 10000000
174
+ },
175
+ "moneyCustomMax": {
176
+ "label": "Largest chosen amount (pence)",
177
+ "type": "number",
178
+ "default": 50000,
179
+ "min": 100,
180
+ "max": 10000000
181
+ },
182
+ "activityTypes": {
183
+ "label": "Activity vouchers",
184
+ "description": "One per line: event id, ticket type id, price in pence, label. For example: ev_123, tt_456, 3500, One clay session.",
185
+ "type": "string",
186
+ "default": "",
187
+ "multiline": true
188
+ },
189
+ "delivery": {
190
+ "label": "Delivery options",
191
+ "description": "Any of email-buyer, email-recipient, print, post (comma-separated).",
192
+ "type": "string",
193
+ "default": "email-buyer,email-recipient,print"
194
+ },
195
+ "scheduledSend": {
196
+ "label": "Buyers may pick the day the recipient's email goes out",
197
+ "type": "boolean",
198
+ "default": true
199
+ },
200
+ "currency": {
201
+ "label": "Currency",
202
+ "description": "ISO code, e.g. GBP. One currency per site.",
203
+ "type": "string",
204
+ "default": "GBP"
205
+ },
206
+ "turnstileSiteKey": {
207
+ "label": "Turnstile site key",
208
+ "type": "string"
209
+ },
210
+ "turnstileSecretKey": {
211
+ "label": "Turnstile secret key",
212
+ "type": "secret"
213
+ },
214
+ "stripeSecretKey": {
215
+ "label": "Stripe secret key",
216
+ "description": "Ideally a restricted key with write access to Checkout Sessions and Refunds. Stored encrypted.",
217
+ "type": "secret"
218
+ },
219
+ "stripeWebhookSecret": {
220
+ "label": "Stripe webhook signing secret",
221
+ "description": "The whsec_ secret of the site's Stripe webhook endpoint. Vouchers are issued only through it. Stored encrypted.",
222
+ "type": "secret"
223
+ },
224
+ "pairingMasterKey": {
225
+ "label": "Pairing master key",
226
+ "description": "Events pairing secrets are derived from this key. Generated when you pair Events; changing it un-pairs Events.",
227
+ "type": "secret"
228
+ },
229
+ "licenceKey": {
230
+ "label": "Pro licence key",
231
+ "description": "Your Events Pro (or Suite) semics1. key. Not needed on local or preview hosts. Stored encrypted.",
232
+ "type": "secret"
233
+ }
234
+ },
235
+ "pages": [
236
+ {
237
+ "path": "/vouchers",
238
+ "label": "Vouchers",
239
+ "icon": "ticket"
240
+ },
241
+ {
242
+ "path": "/to-post",
243
+ "label": "To post",
244
+ "icon": "mail"
245
+ },
246
+ {
247
+ "path": "/unsettled",
248
+ "label": "Unsettled",
249
+ "icon": "calendar"
250
+ },
251
+ {
252
+ "path": "/reports",
253
+ "label": "Voucher reports",
254
+ "icon": "chart"
255
+ },
256
+ {
257
+ "path": "/issue",
258
+ "label": "Issue a voucher",
259
+ "icon": "tag"
260
+ },
261
+ {
262
+ "path": "/pairing",
263
+ "label": "Pairing",
264
+ "icon": "gear"
265
+ }
266
+ ],
267
+ "widgets": []
268
+ }
269
+ }
@@ -0,0 +1,7 @@
1
+ import { SandboxedPlugin } from "emdash/plugin";
2
+
3
+ //#region src/plugin.d.ts
4
+ declare const licence: undefined;
5
+ declare const plugin: SandboxedPlugin;
6
+ //#endregion
7
+ export { plugin as default, licence };
@@ -0,0 +1,5 @@
1
+ function e(e){for(let t of Object.keys(e))e[t]===void 0&&delete e[t];return e}function t(t,n={}){return e({type:`header`,text:t,block_id:n.blockId})}function n(t,n={}){return e({type:`section`,text:t,accessory:n.accessory,block_id:n.blockId})}function r(t,n={}){return e({type:`context`,text:t,block_id:n.blockId})}function i(t,n={}){return e({type:`fields`,fields:t,block_id:n.blockId})}function a(t,n={}){return e({type:`stats`,items:t,block_id:n.blockId})}function o(e,t,n){return n===void 0?{key:e,label:t}:{key:e,label:t,format:n}}function s(t){return e({type:`table`,columns:t.columns,rows:t.rows,page_action_id:t.pageActionId,next_cursor:t.nextCursor,empty_text:t.emptyText,block_id:t.blockId})}function c(t){return e({type:`code`,code:t.code,language:t.language,block_id:t.blockId})}const l={error:`error`,warning:`alert`,info:`default`};function u(t){if(t.title===void 0&&t.description===void 0)throw Error(`banner needs a title or a description`);return e({type:`banner`,title:t.title,description:t.description,variant:t.variant,block_id:t.blockId})}function d(e){return u({title:e.title,description:e.message,variant:l[e.severity],blockId:e.blockId})}function f(e){return e.filter(e=>!!e).map(d)}function p(t,n={}){return e({type:`actions`,elements:t,block_id:n.blockId})}function m(t,n,r={}){return e({type:`button`,action_id:t,label:n,style:r.style,value:r.value,confirm:r.confirm})}function h(t,n,r={}){return e({type:`link`,label:t,target:n,appearance:r.appearance})}function g(t,n,r={}){return e({type:`text_input`,action_id:t,label:n,placeholder:r.placeholder,initial_value:r.initialValue,multiline:r.multiline})}function _(t,n,r,i={}){return e({type:`select`,action_id:t,label:n,options:r,initial_value:i.initialValue,optionsRoute:i.optionsRoute})}function v(t){return e({type:`form`,fields:t.fields,submit:{label:t.submit.label,action_id:t.submit.actionId},block_id:t.blockId})}const y=e=>crypto.getRandomValues(new Uint8Array(e));function b(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function x(e){return Array.from(new Uint8Array(e),e=>e.toString(16).padStart(2,`0`)).join(``)}function ee(e){return crypto.subtle.importKey(`raw`,new Uint8Array(e),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`])}function S(e){return{ok:!0,value:e}}function C(e){return{ok:!1,error:e}}function te(...e){let t={};for(let n of e)for(let[e,[r,i]]of Object.entries(n??{}))t[e]=Object.freeze({status:r,message:i});return Object.freeze(t)}const ne={BAD_REQUEST:[400,`The request was not valid.`],NOT_FOUND:[404,`Not found.`],RATE_LIMITED:[429,`Too many requests. Please try again shortly.`],TURNSTILE_FAILED:[403,`The bot check failed. Please try again.`],TOKEN_IN_QUERY:[400,`Tokens must not be sent in the URL.`],INTERNAL:[500,`Something went wrong. Please try again later.`],SOLD_OUT:[409,`This event is sold out.`],HOLD_EXPIRED:[409,`Your hold has expired. Please start again.`],SALES_CLOSED:[409,`Sales for this event are closed.`],BOOKING_TOKEN_INVALID:[404,`Not found.`],BOOKING_TOKEN_EXPIRED:[401,`This booking link has expired.`],CANCEL_NOT_ALLOWED:[409,`This booking can no longer be cancelled online.`]},re={ENVELOPE_NOT_FOUND:[404,`Document not found.`],ENVELOPE_CLOSED:[409,`This document can no longer be signed.`],SIGNING_TOKEN_INVALID:[401,`This signing link is not valid.`],SIGNING_TOKEN_EXPIRED:[401,`This signing link has expired.`],SESSION_REQUIRED:[409,`Please reopen the signing page and try again.`],SIGNED_PDF_PENDING:[409,`The signed document is still being prepared. Please try again in a minute.`],SIGNING_OUT_OF_TURN:[409,`Another signer has to sign first. You will get an email when it is your turn.`],OTP_REQUIRED:[403,`Enter the code we emailed you before signing.`],DECLINE_NOT_ALLOWED:[403,`This document must be signed. It cannot be declined.`]},ie={TOKEN_TOO_LARGE:[413,`The token is too large.`],TOKEN_MALFORMED:[400,`The token is malformed.`],CLIENT_UNKNOWN:[401,`The client is not recognised.`],CLIENT_REVOKED:[403,`The client has been revoked.`],TOKEN_INVALID:[401,`The token is not valid.`],PAYLOAD_INVALID:[400,`The token payload is not valid.`],TOKEN_LIFETIME_EXCEEDED:[400,`The token lifetime is too long.`],TOKEN_NOT_YET_VALID:[401,`The token is not valid yet.`],TOKEN_EXPIRED:[401,`The token has expired.`],RETURN_URL_NOT_ALLOWED:[400,`The return URL is not allowed.`],TOKEN_REPLAYED:[409,`The token has already been used.`],PAIRING_MALFORMED:[400,`The pairing string is malformed.`],TEMPLATE_NOT_FOUND:[404,`Template not found.`],REF_UNKNOWN:[404,`Reference not found.`]},ae={PAYMENT_FAILED:[402,`The payment could not be completed.`]},oe={LICENCE_INVALID:[403,`The licence is not valid.`],LICENCE_EXPIRED:[403,`The licence has expired.`]},se={VOUCHER_INVALID:[400,`This voucher code is not valid.`],VOUCHER_NO_BALANCE:[409,`This voucher has nothing left to spend.`],VOUCHER_WRONG_ACTIVITY:[409,`This voucher is for a different activity.`],VOUCHER_CURRENCY:[409,`This voucher is in a different currency.`],VOUCHER_UNAVAILABLE:[503,`Gift vouchers cannot be used right now. You can still book without one.`]},ce=te(ne,re,ie,ae,oe),le=te(ne,ie,ae,oe,se);function ue(e){if(typeof e==`string`)return e;if(typeof e==`object`&&e&&`code`in e)return e.code}function de(e,t){let n=ue(t),r=typeof n==`string`&&Object.hasOwn(e,n)?n:`INTERNAL`,{status:i,message:a}=e[r];return{status:i,body:{ok:!1,error:{code:r,message:a}}}}function fe(e){return de(ce,e)}const pe=Object.freeze({"events/checkout":`new`,"events/promo-create":`new`,"events/reminder-schedule":`new`,"esign/envelope-send":`new`,"events/refund":`in-flight`,"events/checkin":`in-flight`,"events/reports":`in-flight`,"esign/waiver-sign":`in-flight`,"esign/reports":`in-flight`});function me(e,t){return t===`in-flight`||e.ok?S(!0):C(e.error)}const he=new TextEncoder;function ge(e=y){let t=e(32);if(t.length!==32)throw Error(`Need 32 bytes.`);return b(t)}function _e(e,t){let n=he.encode(e),r=he.encode(t),i=Math.max(n.length,r.length),a=n.length^r.length;for(let e=0;e<i;e++)a|=(n[e]??0)^(r[e]??0);return a===0}Object.freeze({intent:`esign-intent/1`,receipt:`esign-receipt/1`,lookup:`esign-lookup/1`,catalog:`esign-catalog/1`}),Object.freeze({intent:8192,receipt:2048,lookup:1024,catalog:1024});const ve=/^[A-Za-z0-9_-]{4,64}$/,ye=/^[A-Za-z0-9_-]*$/,be=new TextEncoder;function w(e){if(!ye.test(e)||e.length%4==1)return;let t=atob(e.replace(/-/g,`+`).replace(/_/g,`/`)),n=Uint8Array.from(t,e=>e.charCodeAt(0));return b(n)===e?n:void 0}async function xe(e){if(e.length!==32)throw Error(`Client secret must be 32 bytes.`);return ee(e)}async function Se(e,t,n){let r=await crypto.subtle.sign(`HMAC`,await xe(e),be.encode(`${t}.${n}`));return b(new Uint8Array(r))}const T=/^[A-Za-z0-9_-]{1,64}$/,Ce=/^[A-Za-z0-9._:-]{1,128}$/;function we(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function E(e,t){return typeof e==`string`&&t.test(e)}function D(e){return Number.isSafeInteger(e)&&e>=0}function Te(e){if(!(e[0]===239&&e[1]===187&&e[2]===191))try{let t=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(e));return we(t)?t:void 0}catch{return}}const Ee=`semics1`,De=[`events`,`esign`,`suite`],Oe=[`events`,`esign`],ke=/^\d{4}-\d{2}-\d{2}$/,Ae=/^[A-Za-z0-9.:[\]-]+$/,je=[`localhost`,`127.0.0.1`,`[::1]`],Me=[`.localhost`,`.workers.dev`,`.pages.dev`],Ne=new TextEncoder,Pe=new TextDecoder(`utf-8`,{fatal:!0});function Fe(e){if(!Ae.test(e))return;let t;try{t=new URL(`http://${e}`).hostname}catch{return}return t.replace(/\.$/,``)||void 0}function Ie(e){return je.includes(e)||Me.some(t=>e.endsWith(t))}function Le(e){if(typeof e!=`string`||!ke.test(e))return!1;let t=Date.parse(`${e}T00:00:00Z`);return!Number.isNaN(t)&&new Date(t).toISOString().slice(0,10)===e}function Re(e){return typeof e==`string`&&Fe(e)===e}function ze(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let{v:t,product:n,edition:r,sites:i,customer:a,issuedAt:o,updatesUntil:s}=e;return t===1&&De.includes(n)&&r===`pro`&&Array.isArray(i)&&i.length>=1&&i.length<=100&&i.every(Re)&&typeof a==`string`&&a.length>=1&&a.length<=200&&Le(o)&&Le(s)&&o<=s?e:void 0}function Be(e){let t=w(e);if(t)try{return ze(JSON.parse(Pe.decode(t)))}catch{return}}async function Ve(e,t){let n=e?.trim()??``;if(n.length>4096)return;let r=n.split(`.`);if(r.length!==3||r[0]!==Ee)return;let[,i,a]=r,o=w(a);if(o?.length!==64)return;let s=Ne.encode(`${Ee}.${i}`);if(await crypto.subtle.verify(`Ed25519`,t,new Uint8Array(o),s))return Be(i)}function He(e){let t=w(e.publicKey);if(t?.length!==32)throw Error(`Licence public key must be 32 raw Ed25519 bytes, base64url.`);if(!Le(e.buildDate))throw Error(`Licence build date must be YYYY-MM-DD.`);if(!Oe.includes(e.product))throw Error(`Licence build product must be events or esign.`);let{product:n,buildDate:r}=e,i,a=()=>(i??=crypto.subtle.importKey(`raw`,new Uint8Array(t),{name:`Ed25519`},!1,[`verify`]),i);return async({key:e,hostname:t})=>{let i=Fe(t);if(i===void 0)return C(`LICENCE_INVALID`);if(Ie(i))return S({kind:`exempt`,hostname:i});let o=await Ve(e,await a());return!(o!==void 0&&(o.product===n||o.product===`suite`))||!o.sites.includes(i)?C(`LICENCE_INVALID`):r>o.updatesUntil?C(`LICENCE_EXPIRED`):S({kind:`licensed`,hostname:i,licence:o})}}function O(e){return new Intl.NumberFormat(`en`,{style:`currency`,currency:e}).resolvedOptions().maximumFractionDigits??2}function Ue(e){if(!Number.isSafeInteger(e))throw RangeError(`Not an integer: ${e}`)}function k(e,t,n=`en-GB`){Ue(e);let r=O(t);return new Intl.NumberFormat(n,{style:`currency`,currency:t}).format(e/10**r)}function We(e,t){Ue(e);let n=O(t),r=e<0?`-`:``,i=String(Math.abs(e)).padStart(n+1,`0`);return n===0?r+i:`${r}${i.slice(0,-n)}.${i.slice(-n)}`}function Ge(e,t){let n=O(t),r=/^(-?)(\d+)(?:\.(\d*))?$/.exec(e.trim().replace(/,/g,``));if(!r)throw RangeError(`Not decimal: ${e}`);let i=r[3]??``;if(i.length>n)throw RangeError(`Too many decimals: ${e}`);let a=Number(`${r[2]}${i.padEnd(n,`0`)}`);return Ue(a),r[1]?-a:a}function Ke(e){let t=e.kind;return{to:e.to,subject:e.subject,...typeof t==`string`?{kind:t}:{}}}function qe(e){return Math.min(6e4*2**(e-1),36e5)}async function Je(e){let t={sent:0,failed:0,gaveUp:0,remainingHint:!1};if(!e.email)return t;let n=Math.max(1,e.maxPerTick??6),{items:r}=await e.storage.query({status:`pending`,nextAttemptAtLte:e.now,limit:n+1});if(r.length===0)return t;let i=[],a=0,o=0,s=0;for(let{id:t,data:c}of r.slice(0,n)){let n=c.attempts+1;try{await e.email.send(c.message),a++;let r=Ke(c.message);i.push({id:t,data:{...c,message:r,status:`sent`,attempts:n,sentAt:e.now}})}catch(r){let a=(r instanceof Error?r.message:String(r)).replace(/\S*:\/\/\S*|[\w-]{20,}/g,`…`).slice(0,300);n>=5?(s++,i.push({id:t,data:{...c,message:Ke(c.message),status:`failed`,attempts:n,lastError:a}})):(o++,i.push({id:t,data:{...c,attempts:n,lastError:a,nextAttemptAt:e.now+qe(n)}}))}}return await e.storage.putMany(i),{sent:a,failed:o,gaveUp:s,remainingHint:r.length>n}}new TextEncoder;function Ye(e=y){let t=e(16);if(t.length!==16)throw Error(`Random source must return 16 bytes.`);let n=`cl_`;for(let e of t)n+=`abcdefghijklmnopqrstuvwxyz234567`[e&31];return n}async function Xe(e,t){let n=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(`${t}:${e}`));return x(new Uint8Array(n).slice(0,16))}async function Ze(e,t,n,r){let i=Math.floor(n/(r.windowSeconds*1e3)),a=`rl:${r.scope}:${await Xe(t,r.salt)}:${i}`,o=Number.parseInt(await e.get(a)??`0`,10),s=Number.isFinite(o)&&o>0?o:0;return s>=r.limit?C(`RATE_LIMITED`):(await e.set(a,`${s+1}:${(i+1)*r.windowSeconds}`,{ttlSeconds:r.windowSeconds*2}),S({remaining:r.limit-s-1}))}async function Qe(e,t,n){let r=(await e.list(`rl:`)).filter(({value:e})=>!(Number(String(e).split(`:`)[1])*1e3>t)).map(({key:e})=>e);for(let t of r.slice(0,n))await e.delete(t);return{calls:1+Math.min(n,r.length),more:r.length>n}}const $e=`cron:scheduled:v11`,et=`ensure-schedules`;async function tt(e,t){try{if(await e.kv.get($e))return;await e.cron?.schedule(et,{schedule:new Date(t).toISOString()})}catch{}}async function nt(e,t){let n=e.cron;if(!n)return!1;for(let e of t)await n.schedule(e.name,{schedule:e.schedule});return!0}async function rt(e,t,n){await nt(e,t)&&await e.kv.set($e,{at:new Date(n).toISOString()})}async function it(e,t,n,r){return e.name===et?(await rt(t,n,r),!0):!1}function at(e,t=()=>!0,n=Date.now){let r=e.handler,i=async(e,i)=>(t(e?.input)&&await tt(i,n()),r(e,i));return{...e,handler:i}}async function ot(e,t){if(!t.token)return C(`TURNSTILE_FAILED`);let n=new URLSearchParams({secret:t.secret,response:t.token});t.remoteIp&&n.set(`remoteip`,t.remoteIp);try{let t=await e(`https://challenges.cloudflare.com/turnstile/v0/siteverify`,{method:`POST`,body:n});return t.ok&&(await t.json()).success===!0?S(!0):C(`TURNSTILE_FAILED`)}catch{return C(`TURNSTILE_FAILED`)}}const st=Object.freeze({hold:`voucher-hold/1`,settle:`voucher-settle/1`,lookup:`voucher-lookup/1`}),ct=Object.freeze({hold:1024,settle:1024,lookup:1024}),lt=1e7,ut=/^[A-Z]{3}$/,dt=new Set([`confirmed`,`released`,`credited`]);function ft(e,t){return D(e)&&e>=t&&e<=lt}function pt(e){let t=e.kind===`activity`,n=e.line;return E(e.rid,T)&&D(e.iat)&&D(e.exp)&&e.exp>e.iat&&E(e.ref,Ce)&&(t||e.kind===`money`)&&ft(e.amountMinor,t?0:1)&&E(e.currency,ut)&&(t?we(n)&&E(n.eventId,T)&&E(n.ticketTypeId,T):!Object.hasOwn(e,`line`))}function mt(e){let t=e.outcome===`credited`;return E(e.rid,T)&&E(e.ref,Ce)&&dt.has(e.outcome)&&(t?ft(e.creditMinor,1):e.creditMinor===0)&&D(e.at)}const ht={hold:pt,settle:mt,lookup:e=>D(e.ts)&&E(e.ref,Ce)&&E(e.rid,T)};function gt(e,t){return!we(t)||t.v!==1||t.typ!==e||!E(t.cid,ve)||!ht[e](t)?C(`PAYLOAD_INVALID`):e===`hold`&&t.exp-t.iat>1800?C(`TOKEN_LIFETIME_EXCEEDED`):S(t)}async function _t(e,t){let{token:n}=t;if(typeof n!=`string`)return C(`TOKEN_MALFORMED`);if(n.length>ct[e])return C(`TOKEN_TOO_LARGE`);let r=n.split(`.`);if(r.length!==2)return C(`TOKEN_MALFORMED`);let[i,a]=r,o=w(i);if(o===void 0||o.length===0||a.length!==43||w(a)===void 0)return C(`TOKEN_MALFORMED`);let s=Te(o);if(s===void 0||!E(s.cid,ve))return C(`TOKEN_MALFORMED`);let c=await t.getClient(s.cid);return c===void 0?C(`CLIENT_UNKNOWN`):c.revoked?C(`CLIENT_REVOKED`):_e(await Se(c.secret,st[e],i),a)?S(s):C(`TOKEN_INVALID`)}function vt(e,t){return e===`hold`?[t.iat,t.exp+60]:e===`settle`?[t.at,void 0]:[t.ts,t.ts+300+60]}async function yt(e,t){let n=await _t(e,t);if(!n.ok)return n;let r=gt(e,n.value);if(!r.ok)return r;let i=Math.floor(t.now/1e3),[a,o]=vt(e,n.value);return a>i+60?C(`TOKEN_NOT_YET_VALID`):o!==void 0&&i>o?C(`TOKEN_EXPIRED`):r}const bt=new TextEncoder;async function xt(e,t,n){let r=gt(e,t);if(!r.ok)throw Error(`Cannot sign ${e} token: ${r.error}.`);let i=b(bt.encode(JSON.stringify(t))),a=`${i}.${await Se(n,st[e],i)}`;if(a.length>ct[e])throw Error(`Cannot sign ${e} token: TOKEN_TOO_LARGE.`);return a}function St(e,t){if(!ve.test(e))throw Error(`Client id must match the voucher/1 clientId grammar.`);if(t.length!==32)throw Error(`Client secret must be 32 bytes.`);return`voucher1.${e}.${b(t)}`}const Ct=`0123456789ABCDEFGHJKMNPQRSTVWXYZ`,wt=/[ \t\r\n-]/g,Tt=/^[0-9A-Za-z]{16}$/,Et={O:`0`,I:`1`,L:`1`};function Dt(e=y){let t=e(10);if(t.length!==10)throw Error(`Random source must return 10 bytes.`);let n=``,r=0,i=0;for(let e of t)for(r=(r<<8|e)&4095,i+=8;i>=5;)i-=5,n+=Ct[r>>i&31];return n}function Ot(e){if(typeof e!=`string`||e.length>64)return;let t=e.replace(wt,``);if(!Tt.test(t))return;let n=``;for(let e of t.toUpperCase())n+=Et[e]??e;for(let e of n)if(!Ct.includes(e))return;return n}function kt(e){let t=Ot(e);return t&&`${t.slice(0,4)}-${t.slice(4,8)}-${t.slice(8,12)}-${t.slice(12)}`}async function At(e){let t=Ot(e);if(t!==void 0)return x(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(t)))}function jt(e){try{return e?new URL(e).host:``}catch{return``}}function Mt(e){if(!e.ok)return e.error===`LICENCE_EXPIRED`?{status:`expired`,code:`LICENCE_EXPIRED`}:{status:`invalid`,code:`LICENCE_INVALID`};let t=e.value;if(t.kind===`exempt`)return{status:`exempt`,hostname:t.hostname};let{customer:n,sites:r,updatesUntil:i}=t.licence;return{status:`licensed`,hostname:t.hostname,customer:n,sites:r,updatesUntil:i}}function Nt(e){return e===`new`||e===`in-flight`?e:pe[e]}function Pt(e,t={}){let n=He(e),r=async e=>n({key:await e.settings.get(`licenceKey`),hostname:jt(e.site?.url)});return{build:e,check:r,gate(e,t){let n=Nt(e);if(n===`in-flight`)return t;let i=typeof t==`function`?t:t.handler,a=async(e,t)=>{let a=me(await r(t),n);return a.ok?i(e,t):fe(a.error).body};return typeof t==`function`?a:{...t,handler:a}},statusRoute(){return{handler:async(n,i)=>({ok:!0,edition:`pro`,product:e.product,buildDate:e.buildDate,devKey:t.devKey??!1,licence:Mt(await r(i))})}}}}function Ft(e,t,n,r,i){let a={Authorization:`Bearer ${t}`};return r&&(a[`Content-Type`]=`application/x-www-form-urlencoded`),i&&(a[`Idempotency-Key`]=i),a[`Stripe-Version`]=`2024-06-20`,e.fetch(`https://api.stripe.com/v1/${n}`,{method:r?`POST`:`GET`,headers:a,...r?{body:r.toString()}:{}})}const It=new TextEncoder;async function Lt(e,t,n,r,i=300){if(!t)return C(`SIGNATURE_MISSING`);let a=null,o=[];for(let e of t.split(`,`)){let t=e.indexOf(`=`),n=e.slice(0,t).trim(),r=e.slice(t+1).trim();t<1||!r||(n===`t`&&/^\d{1,12}$/.test(r)?a=Number(r):n===`v1`&&/^[0-9a-f]{64}$/i.test(r)&&o.push(r.toLowerCase()))}if(a===null||o.length===0||!n)return C(`SIGNATURE_INVALID`);let s=await crypto.subtle.importKey(`raw`,It.encode(n),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]),c=x(await crypto.subtle.sign(`HMAC`,s,It.encode(`${a}.${e}`)));return o.reduce((e,t)=>_e(t,c)||e,!1)?Math.abs(r-a)>i?C(`SIGNATURE_STALE`):S(!0):C(`SIGNATURE_INVALID`)}function Rt(e={}){let t=[];return new Headers(e.headers).forEach((e,n)=>t.push([n,e])),{__emdashPluginResponse:!0,status:e.status??200,headers:t,body:e.body??null}}const A=1e7,zt=/^[A-Za-z0-9_-]{1,64}$/,Bt=[`money`,`activity`],Vt=[`email-buyer`,`email-recipient`,`print`,`post`],j=e=>typeof e==`string`,M=(e,t,n)=>typeof e==`number`&&Number.isSafeInteger(e)&&e>=t&&e<=n;function Ht(e,t,n){return(j(e)?e:n).split(`,`).map(e=>e.trim()).filter(e=>t.includes(e))}const Ut=(e,t,n,r)=>M(e,t,n)?e:r;function Wt(e){if(!j(e))return[];let t=[];for(let n of e.split(/\r?\n/)){let e=n.split(`,`);if(e.length<4)continue;let[r,i,a]=e.slice(0,3).map(e=>e.trim()),o=e.slice(3).join(`,`).trim(),s=/^\d{1,8}$/.test(a)?Number(a):0;!zt.test(r)||!zt.test(i)||!M(s,1,A)||!o||t.push({eventId:r,ticketTypeId:i,priceMinor:s,label:o.slice(0,120)})}return t}function Gt(e){let t=j(e)?e.trim().toUpperCase():``;try{if(/^[A-Z]{3}$/.test(t))return O(t),t}catch{}return`GBP`}function Kt(e){let t=(j(e.moneyAmounts)?e.moneyAmounts:`2500,5000,10000`).split(`,`).map(e=>e.trim()).filter(e=>/^\d{1,8}$/.test(e)).map(Number).filter(e=>M(e,1,A)),n=Ut(e.moneyCustomMin,1,A,1e3),r=Ut(e.moneyCustomMax,1,A,5e4);return{kinds:Ht(e.kinds,Bt,`money,activity`),validityMonths:Ut(e.validityMonths,1,60,12),moneyAmounts:t,moneyCustom:e.moneyCustom===!1||n>r?null:{min:n,max:r},activityTypes:Wt(e.activityTypes),delivery:Ht(e.delivery,Vt,`email-buyer,email-recipient,print`),scheduledSend:e.scheduledSend!==!1,currency:Gt(e.currency)}}function qt(e){let t={};for(let{key:n,value:r}of e)t[n]=r;return t}function Jt(e,t){let{kinds:n,moneyAmounts:r,moneyCustom:i,activityTypes:a,delivery:o,scheduledSend:s,currency:c,validityMonths:l}=e,u=t.turnstileSiteKey;return{kinds:n,moneyAmounts:r,moneyCustom:i,activities:a,delivery:o,scheduledSend:s,currency:c,validityMonths:l,...j(u)&&u?{turnstileSiteKey:u}:{}}}const Yt={publicKey:`Trv2FjPQUPvm0izcO5IyqAZ5hse1-WShywKnN2s3ijg`,product:`events`,buildDate:`2026-11-14`},N=(e,t={})=>({...de(le,e).body,...t}),P=async e=>qt(await e.list()),Xt={purchase:{limit:10,windowSeconds:600},hold:{limit:20,windowSeconds:600},settle:{limit:120,windowSeconds:600},check:{limit:30,windowSeconds:600}};async function F(e,t,n,r,i,a){let o=typeof n.turnstileSecretKey==`string`&&n.turnstileSecretKey?n.turnstileSecretKey:``;return o||=typeof n.ipSalt==`string`&&n.ipSalt?n.ipSalt:``,o||(o=ge(),await t.set(`ipSalt`,o)),!(await Ze({get:async t=>await e.get(t),set:(t,n)=>e.set(t,n)},i??`unknown`,a,{scope:r,salt:o,...Xt[r]})).ok}let Zt;async function Qt(e,t){Zt??=He(Yt);let n=``;try{n=t?new URL(t).host:``}catch{n=``}let r=typeof e.licenceKey==`string`?e.licenceKey:null,i=me(await Zt({key:r,hostname:n}),`new`);return i.ok?null:i.error}function $t(e){return(t,n)=>{if(!e.http)throw Error(`no network`);return e.http.fetch(t,n)}}function en(e){try{let{origin:t}=new URL(e??``);return t===`null`?``:t}catch{return``}}const I=e=>new Date(e).toISOString();function tn(e,t){let n=new Date(e),r=new Date(Date.UTC(n.getUTCFullYear(),n.getUTCMonth()+t,1)),i=new Date(Date.UTC(r.getUTCFullYear(),r.getUTCMonth()+1,0)).getUTCDate();return r.setUTCDate(Math.min(n.getUTCDate(),i)),r.setUTCHours(n.getUTCHours(),n.getUTCMinutes(),n.getUTCSeconds(),n.getUTCMilliseconds()),r.getTime()}function nn(e,t,n){let r=e.kind===`money`;return{kind:e.kind,currency:e.currency,amountMinor:e.amountMinor,balanceMinor:r?e.amountMinor:0,usesLeft:r?0:1,heldMinor:0,holds:[],activity:e.activity,status:`active`,last4:t.code.slice(-4),issuedAt:I(n),expiresAt:I(tn(n,e.validityMonths)),orderId:t.orderId,soldOnline:!0,buyerName:e.buyer.name,buyerEmail:e.buyer.email,recipientName:e.recipient?.name??null,recipientEmail:e.recipient?.email??null,message:e.message,delivery:e.delivery,address:e.address,sendOn:e.sendOn,version:1,ledger:[{at:I(n),type:`issue`,amountMinor:e.amountMinor,by:`system`,note:`Sold online`}]}}function rn(e,t){return e.status===`void`||e.status===`expired`||!(Date.parse(e.expiresAt)>t)}function L(e){return e.status===`void`||e.status===`expired`?e.status:(e.kind===`money`?e.balanceMinor:e.usesLeft)===0&&e.heldMinor===0?`spent`:`active`}const an=e=>({kind:e.kind,currency:e.currency,amountMinor:e.amountMinor,balanceMinor:e.balanceMinor,usesLeft:e.usesLeft,heldMinor:e.heldMinor,label:e.activity?.label??null,status:e.status,expiresAt:e.expiresAt,toName:e.recipientName,fromName:e.buyerName,message:e.message});function on(e,t,n){if(rn(e,n))return C(`VOUCHER_INVALID`);if(t.currency&&t.currency!==e.currency)return C(`VOUCHER_CURRENCY`);if(t.lines){let n=e.activity;if(e.kind===`activity`&&!t.lines.some(e=>e.eventId===n?.eventId&&e.ticketTypeId===n?.ticketTypeId))return C(`VOUCHER_WRONG_ACTIVITY`);if((e.kind===`money`?e.balanceMinor:e.usesLeft)<1&&e.holds.length===0)return C(`VOUCHER_NO_BALANCE`)}return S(an(e))}function sn(e,t,n,r){if(rn(e,n))return C(`VOUCHER_INVALID`);if(e.currency!==t.currency)return C(`VOUCHER_CURRENCY`);let i=Math.floor(n/1e3),a,o;if(e.kind===`activity`){let n=e.activity,r=t.lines.find(e=>e.eventId===n?.eventId&&e.ticketTypeId===n?.ticketTypeId&&e.qty>=1);if(!n||!r)return C(`VOUCHER_WRONG_ACTIVITY`);a={eventId:n.eventId,ticketTypeId:n.ticketTypeId},o=r.unitMinor}else{if(t.dueMinor<1)return C(`BAD_REQUEST`);o=Math.min(e.balanceMinor,t.dueMinor)}let s=e.holds.find(e=>e.ref===t.ref&&e.exp>i);if(s)return S({reused:!0,hold:s,line:a});if((e.kind===`money`?e.balanceMinor:e.usesLeft)<1||e.kind===`money`&&o<1)return C(`VOUCHER_NO_BALANCE`);let c={rid:r,ref:t.ref,amountMinor:o,iat:i,exp:i+1800},l={...e,balanceMinor:e.kind===`money`?e.balanceMinor-o:e.balanceMinor,usesLeft:e.kind===`money`?e.usesLeft:0,heldMinor:e.heldMinor+o,holds:[...e.holds,c],version:e.version+1,ledger:[...e.ledger,{at:I(n),type:`hold`,amountMinor:o,rid:r,ref:t.ref,by:`booking`}]};return S({reused:!1,hold:c,line:a,next:{...l,status:L(l)}})}function cn(e,t,n){let r={next:null,state:e.state},i=()=>e.late.some(e=>e.outcome===t.outcome&&e.creditMinor===t.creditMinor)?r:{next:{...e,late:[...e.late,{outcome:t.outcome,creditMinor:t.creditMinor,at:t.at}]},state:e.state},a=Math.min(t.creditMinor,e.amountMinor);switch(t.outcome){case`confirmed`:return e.state===`released`?i():e.state===`held`?{next:{...e,state:`confirmed`},state:`confirmed`}:r;case`released`:return e.state===`confirmed`||e.state===`credited`?i():e.state===`released`||n<=e.exp+60?r:{next:{...e,state:`released`},state:`released`};case`credited`:return e.state===`released`?i():e.state===`credited`&&a<=e.creditedMinor?r:{next:{...e,state:`credited`,creditedMinor:Math.max(a,e.creditedMinor)},state:`credited`}}}function ln(e,t,n,r){if(n.state===`held`)return null;let i=I(r),a=e.kind===`money`,o=e,s=e.holds.find(e=>e.rid===t);if(s){let e=n.state===`released`;o={...o,holds:o.holds.filter(e=>e.rid!==t),heldMinor:o.heldMinor-s.amountMinor,balanceMinor:e&&a?o.balanceMinor+s.amountMinor:o.balanceMinor,usesLeft:e&&!a?1:o.usesLeft,ledger:[...o.ledger,{at:i,type:e?`release`:`spend`,amountMinor:s.amountMinor,rid:t,ref:s.ref,by:`booking`}]}}if(n.state===`credited`){let e=o.ledger.filter(e=>e.type===`credit`&&e.rid===t),r=e.reduce((e,t)=>e+t.amountMinor,0),s=a?n.creditedMinor-r:e.length===0?n.creditedMinor:0;(s>0||!a&&e.length===0)&&(o={...o,balanceMinor:a?o.balanceMinor+s:o.balanceMinor,usesLeft:a?o.usesLeft:1,ledger:[...o.ledger,{at:i,type:`credit`,amountMinor:s,rid:t,ref:n.ref,by:`booking`}]})}return o===e?null:{...o,status:L(o),version:e.version+1}}const R=e=>new Date(e).toISOString(),z=(e,t,n,r,i,a={})=>({at:R(e),type:t,amountMinor:n,by:r,note:i,...a}),B=(e,t,n={})=>({...e,...n,version:e.version+1,ledger:[...e.ledger,t]}),V=e=>e.trim()===``;function un(e){let t=e??{};for(let e of[t.email,t.name,t.id])if(typeof e==`string`&&e.trim())return e.trim().slice(0,120);return`staff`}function dn(e){return e.heldMinor>0||e.holds.length>0||e.ledger.some(e=>[`spend`,`manual`,`credit`].includes(e.type))}function fn(e,t,n){return e.status===`void`?C(`This voucher is already void.`):V(t.reason)?C(`Give a reason.`):dn(e)?C(`A voucher that has been used can't be voided.`):S(B(e,z(n,`void`,e.kind===`money`?e.balanceMinor:e.usesLeft>0?e.amountMinor:0,t.by,t.reason.trim()),{status:`void`}))}function pn(e,t,n){if(e.status===`void`)return C(`This voucher is void.`);if(e.status===`spent`)return C(`This voucher is spent: there is nothing left to extend.`);if(V(t.reason))return C(`Give a reason.`);let r=Date.parse(`${t.date}T23:59:59.000Z`);if(!/^\d{4}-\d{2}-\d{2}$/.test(t.date)||Number.isNaN(r)||R(r).slice(0,10)!==t.date)return C(`Enter the new expiry as a date, YYYY-MM-DD.`);if(r<=Date.parse(e.expiresAt))return C(`The new expiry must be after the current one.`);if(r<=n)return C(`The new expiry must be in the future.`);let i=B(e,z(n,`extend`,0,t.by,`${t.reason.trim()} (until ${t.date})`),{expiresAt:R(r)});return S({...i,status:L({...i,status:`active`})})}function mn(e,t,n){if(e.status===`void`)return C(`This voucher is void.`);if(e.status===`spent`)return C(`This voucher is spent: there is nothing left to redeem.`);if(rn(e,n))return C(`This voucher has expired: extend it first.`);if(V(t.note))return C(`Add a note (what was it spent on?).`);let r=t.note.trim();if(e.kind===`activity`){if(e.usesLeft<1)return C(`This voucher has no use left (a booking holds it).`);let i=B(e,z(n,`manual`,e.amountMinor,t.by,r),{usesLeft:0});return S({...i,status:L(i)})}let i=t.amountMinor;if(!M(i,1,A))return C(`Enter an amount above zero.`);if(i>e.balanceMinor)return C(`That is more than the balance available to spend.`);let a=B(e,z(n,`manual`,i,t.by,r),{balanceMinor:e.balanceMinor-i});return S({...a,status:L(a)})}function hn(e,t,n,r){let i=e.holds.find(e=>e.rid===t);if(!i)return C(`That hold is not on this voucher (it may already be settled).`);if(V(n.reason))return C(`Give a reason.`);if(Math.floor(r/1e3)<=i.exp+60)return C(`That hold's checkout window has not ended yet: the booking may still complete.`);let a=e.kind===`money`,o=B(e,z(r,`release`,i.amountMinor,n.by,n.reason.trim(),{rid:t,ref:i.ref}),{holds:e.holds.filter(e=>e.rid!==t),heldMinor:e.heldMinor-i.amountMinor,balanceMinor:a?e.balanceMinor+i.amountMinor:e.balanceMinor,usesLeft:a?e.usesLeft:1});return S({...o,status:L(o)})}function gn(e,t,n){return e.delivery===`post`?e.postedAt?C(`This voucher is already marked as posted.`):S(B(e,z(n,`note`,0,t.by,`Marked as posted`),{postedAt:R(n)})):C(`This is not a voucher to post.`)}function _n(e,t,n){if(e.status===`void`)return C(`This voucher is void.`);if(e.status===`spent`)return C(`This voucher is spent: there is nothing to resend.`);if(V(t.reason))return C(`Give a reason.`);if(e.holds.length>0||e.heldMinor>0)return C(`A booking holds this voucher: release the hold first.`);let r=t.newCode.slice(-4),i=t.reason.trim();return S({old:B(e,z(n,`replace`,0,t.by,`${i} (replaced by the code ending ${r})`),{status:`void`,replacedBy:t.newId}),next:B(e,z(n,`replace`,0,t.by,`${i} (replaces the code ending ${e.last4})`),{last4:r,replaces:t.oldId})})}function vn(e,t,n,r){if(e.kind!==`money`&&e.kind!==`activity`)return C(`Choose the kind of voucher.`);if(V(e.note))return C(`Add a note (why is it being issued?).`);let i=e.holderEmail.trim();if(i&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(i))return C(`That email address does not look right.`);let a=e.validityMonths??t.validityMonths;if(!M(a,1,60))return C(`Valid for between 1 and 60 months.`);let o,s=null;if(e.kind===`money`){if(!M(e.amountMinor,1,A))return C(`Enter an amount between 1p and the largest amount allowed.`);o=e.amountMinor}else{let n=t.activityTypes[e.activityIndex??-1];if(!n)return C(`Choose an activity (set them up in the plugin settings).`);o=n.priceMinor,s={eventId:n.eventId,ticketTypeId:n.ticketTypeId,label:n.label}}let c=e.kind===`money`;return S({kind:e.kind,currency:t.currency,amountMinor:o,balanceMinor:c?o:0,usesLeft:c?0:1,heldMinor:0,holds:[],activity:s,status:`active`,last4:n.slice(-4),issuedAt:R(r),expiresAt:R(tn(r,a)),orderId:null,soldOnline:!1,buyerName:e.holderName.trim()||`Issued by staff`,buyerEmail:i,recipientName:null,recipientEmail:null,message:null,delivery:`print`,address:null,sendOn:null,version:1,ledger:[z(r,`issue`,o,e.by,`Not sold online: ${e.note.trim()}`)]})}function yn(e,t){let n=t.trim().toLowerCase();return[e.buyerName,e.buyerEmail,e.recipientName,e.recipientEmail].some(e=>e?.toLowerCase().includes(n))}function bn(e,t,n){if(t.kind&&e.kind!==t.kind||t.status&&e.status!==t.status)return!1;if(t.expiring){let t=Date.parse(e.expiresAt);if(e.status!==`active`||t<=n||t>n+30*864e5)return!1}return!0}const xn=[`sold`,`redeemed`,`expired`,`voided`,`credited`],Sn=e=>e.replacedBy===void 0,Cn=(e,t)=>Sn(e)&&e.status===`active`&&Date.parse(e.expiresAt)>t,wn=e=>e.sort((e,t)=>e.currency.localeCompare(t.currency));function Tn(e,t){let n=new Map,r=new Map;for(let i of e)if(Cn(i,t))if(i.kind===`money`){if(i.balanceMinor<=0&&i.heldMinor<=0)continue;let e=n.get(i.currency)??{currency:i.currency,count:0,balanceMinor:0,heldMinor:0};e.count++,e.balanceMinor+=i.balanceMinor,e.heldMinor+=i.heldMinor,n.set(i.currency,e)}else{if(i.usesLeft<=0&&i.heldMinor<=0)continue;let e=r.get(i.currency)??{currency:i.currency,count:0,paidMinor:0};e.count++,e.paidMinor+=i.amountMinor,r.set(i.currency,e)}return{money:wn([...n.values()]),activity:wn([...r.values()])}}const En={issue:`sold`,spend:`redeemed`,manual:`redeemed`,credit:`credited`,void:`voided`};function Dn(e,t){let n=new Map,r=(e,t,r,i)=>{let a=`${e}|${t.currency}|${t.kind}|${r}`,o=n.get(a)??{month:e,currency:t.currency,kind:t.kind,move:r,count:0,minor:0};o.count++,o.minor+=i,n.set(a,o)};for(let n of e)if(Sn(n)){for(let e of n.ledger){let t=En[e.type];t&&r(e.at.slice(0,7),n,t,n.kind===`money`||t===`sold`||t===`voided`?e.amountMinor:n.amountMinor)}if(n.status===`expired`||n.status===`active`&&Date.parse(n.expiresAt)<=t){let e=n.kind===`money`?n.balanceMinor:n.usesLeft>0?n.amountMinor:0;e>0&&r(n.expiresAt.slice(0,7),n,`expired`,e)}}let i=e=>xn.indexOf(e.move);return[...n.values()].sort((e,t)=>t.month.localeCompare(e.month)||e.currency.localeCompare(t.currency)||(e.kind===t.kind?0:e.kind===`money`?-1:1)||i(e)-i(t))}const On=/^[=+\-@\t\r=+-@]/,kn=/[",\r\n]/;function An(e){if(e==null)return``;if(typeof e==`number`||typeof e==`boolean`)return String(e);let t=String(e);return On.test(t)&&(t=`'${t}`),kn.test(t)?`"${t.replaceAll(`"`,`""`)}"`:t}const jn=(e,t)=>`${[e,...t].map(e=>e.map(An).join(`,`)).join(`\r
2
+ `)}\r\n`;function Mn(e){return jn([`month`,`currency`,`kind`,`movement`,`count`,`minor`],e.map(e=>[e.month,e.currency,e.kind,e.move,e.count,e.minor]))}function Nn(e,t){return jn([`code_last4`,`kind`,`currency`,`balance_minor`,`held_minor`,`activity`,`price_paid_minor`,`expires_at`,`buyer`,`recipient`],e.filter(e=>Cn(e,t)&&(e.kind===`money`?e.balanceMinor>0||e.heldMinor>0:e.usesLeft>0||e.heldMinor>0)).sort((e,t)=>e.expiresAt.localeCompare(t.expiresAt)).map(e=>{let t=e.kind===`money`;return[e.last4,e.kind,e.currency,t?e.balanceMinor:``,t?e.heldMinor:``,t?``:e.activity?.label??``,t?``:e.amountMinor,e.expiresAt,e.buyerName,e.recipientName??e.recipientEmail??``]}))}const Pn=`pairingMasterKey`,Fn=`pairingClientId`,In=/^[A-Za-z0-9_-]{4,64}$/,Ln=new TextEncoder;function Rn(e){if(typeof e!=`string`)return null;let t=Ln.encode(e.trim());return t.length>=32?t:null}async function zn(e,t){let n=await ee(e);return new Uint8Array(await crypto.subtle.sign(`HMAC`,n,Ln.encode(`voucher-client/1.`+t)))}async function Bn(e){let t=Rn(e[Pn]),n=e[Fn];return!t||typeof n!=`string`||!In.test(n)?null:{cid:n,secret:await zn(t,n)}}function Vn(e,t){let n=Rn(e[Pn]);return async e=>{if(!n)return;let r=await t.get(e);if(r)return{secret:await zn(n,e),revoked:r.revokedAt!==null}}}const Hn=async(e,t)=>{let n=t.storage.clients,r=Rn(await t.settings.get(Pn));if(!r){let e=b(crypto.getRandomValues(new Uint8Array(32)));await t.settings.set(Pn,e),r=Ln.encode(e)}let i=Ye();return await n.put(i,{createdAt:new Date().toISOString(),revokedAt:null}),await t.settings.set(Fn,i),{ok:!0,clientId:i,pairing:St(i,await zn(r,i))}},Un=async(e,t)=>{let n=e.input?.clientId;if(typeof n!=`string`||!In.test(n))return N(`BAD_REQUEST`);let r=await t.storage.clients.updateIf(n,{where:{revokedAt:null},set:{revokedAt:new Date().toISOString()}});return await t.settings.get(Fn)===n&&await t.settings.delete(Fn),r.applied?{ok:!0}:N(`NOT_FOUND`)},Wn={"pairing/create":{handler:Hn},"pairing/revoke":{handler:Un}},Gn=/^[0-9a-f]{64}$/,H=(e,t=300)=>typeof e==`string`?e.trim().slice(0,t):``,U=(e,t)=>({severity:`info`,title:e,...t?{message:t}:{}}),W=(e,t)=>({severity:`error`,title:e,...t?{message:t}:{}}),G=e=>e.slice(0,16).replace(`T`,` `),K=(e,t)=>k(t,e.currency),Kn=e=>e.kind===`money`?`${K(e,e.balanceMinor)} of ${K(e,e.amountMinor)}`:`${e.activity?.label??`Activity`} (paid ${K(e,e.amountMinor)})`,qn=(e,t)=>[e,t].filter(Boolean).join(` · `)||`-`,Jn=[{label:`Any`,value:`any`}],Yn=[...Jn,{label:`Money`,value:`money`},{label:`Activity`,value:`activity`}],Xn=[...Jn,{label:`Active`,value:`active`},{label:`Spent`,value:`spent`},{label:`Expired`,value:`expired`},{label:`Void`,value:`void`}],Zn=[{label:`Any expiry`,value:`any`},{label:`Expiring in 30 days`,value:`30`}],Qn=e=>typeof e==`string`&&e!==`any`?e:``,$n=e=>e.items.map(e=>({id:e.id,v:e.data}));async function er(e,t){let n=e.store.vouchers,r=new Map,i=e=>{for(let{id:t,v:n}of $n(e))r.set(t,n)};if(t.includes(`@`))for(let e of new Set([t,t.toLowerCase()]))for(let t of[`buyerEmail`,`recipientEmail`])i(await n.query({where:{[t]:e},limit:50}));else{let e=t.replaceAll(`-`,``).toUpperCase();if(/^[0-9A-Z]{4}$/.test(e)&&i(await n.query({where:{last4:e},limit:50})),r.size===0){let e;for(let i=0;i<4&&r.size<50;i++){let i=await n.query({orderBy:{expiresAt:`desc`},limit:100,...e?{cursor:e}:{}});for(let{id:e,v:n}of $n(i))yn(n,t)&&r.set(e,n);if(!i.hasMore||!i.cursor)break;e=i.cursor}}}return[...r].map(([e,t])=>({id:e,v:t}))}async function tr(e,n,i){let{now:a}=e,c,l;if(n.q)c=(await er(e,n.q)).filter(e=>bn(e.v,n,a));else{let t=n.expiring?{status:`active`,expiresAt:{gte:new Date(a).toISOString(),lte:new Date(a+30*864e5).toISOString()}}:n.status?{status:n.status}:n.kind?{kind:n.kind}:{},r=await e.store.vouchers.query({...Object.keys(t).length?{where:t}:{},orderBy:{expiresAt:n.expiring?`asc`:`desc`},limit:25,...n.cursor?{cursor:n.cursor}:{}});c=$n(r).filter(e=>bn(e.v,n,a)),l=r.hasMore?r.cursor:void 0}let u=`${n.kind??``}|${n.status??``}|${n.expiring?`30`:``}`;return[t(`Vouchers`),...f([i]),v({blockId:`voucher-search`,fields:[g(`q`,`Search`,{placeholder:`Last 4 characters of the code, a name or an email`,initialValue:n.q??``}),_(`kind`,`Kind`,Yn,{initialValue:n.kind||`any`}),_(`status`,`Status`,Xn,{initialValue:n.status||`any`}),_(`expiring`,`Expiry`,Zn,{initialValue:n.expiring?`30`:`any`})],submit:{label:`Search`,actionId:`vouchers.search`}}),s({columns:[o(`code`,`Code ends`,`code`),o(`kind`,`Kind`),o(`status`,`Status`,`badge`),o(`value`,`Value`),o(`expires`,`Expires`),o(`buyer`,`Buyer`),o(`recipient`,`Recipient`),o(`open`,``,`element`)],rows:c.map(({id:e,v:t})=>({code:t.last4,kind:t.kind,status:t.status,value:Kn(t),expires:G(t.expiresAt),buyer:qn(t.buyerName,t.buyerEmail),recipient:qn(t.recipientName,t.recipientEmail),open:m(`vouchers.open`,`Open`,{value:e})})),pageActionId:`vouchers.page:${u}`,...l?{nextCursor:l}:{},emptyText:n.q?`No voucher matches.`:`No vouchers yet.`,blockId:`voucher-table`}),r(`Search by the last 4 characters of a code, the buyer, the recipient or an email address. Codes are never stored, so staff cannot look one up: use Resend on a voucher to make a new one.`)]}const nr=(e,t=`Open`)=>m(`vouchers.open`,t,{value:e,style:`secondary`});function q(e,a,c,l,u){let d=Math.floor(e.now/1e3),h=c.status!==`void`&&c.status!==`spent`,y=[t(`Voucher ending ${c.last4}`),...f([l,c.replacedBy?{severity:`info`,title:`This code was replaced`,message:`A new code carries this voucher on.`}:null,!c.soldOnline&&{severity:`info`,title:`Not sold online`,message:`Issued by staff, not through the shop.`}]),...c.replacedBy?[p([nr(c.replacedBy,`Open the replacement`)])]:[],...u??[],i([{label:`Kind`,value:c.kind},{label:`Status`,value:c.status},{label:`Value`,value:Kn(c)},...c.heldMinor>0?[{label:`Held by bookings`,value:K(c,c.heldMinor)}]:[],{label:`Expires`,value:G(c.expiresAt)},{label:`Issued`,value:G(c.issuedAt)},{label:`How`,value:c.soldOnline?`Sold online`:`Not sold online`},{label:`Buyer`,value:qn(c.buyerName,c.buyerEmail)},{label:`Recipient`,value:qn(c.recipientName,c.recipientEmail)},{label:`Delivery`,value:c.delivery+(c.address?`: ${c.address}`:``)+(c.postedAt?` (posted)`:``)},...c.message?[{label:`Message`,value:c.message}]:[]])];c.holds.length>0&&y.push(n(`Open holds`),s({columns:[o(`ref`,`Booking`),o(`amount`,`Held`),o(`window`,`Window ends`),o(`state`,`State`)],rows:c.holds.map(e=>({ref:e.ref,amount:K(c,e.amountMinor),window:G(new Date(e.exp*1e3).toISOString()),state:d>e.exp+60?`Past its window`:`Open`})),pageActionId:`vouchers.holds`,blockId:`voucher-holds`}));let b=[];return c.status!==`void`&&!dn(c)&&b.push(v({blockId:`act-void`,fields:[g(`reason`,`Reason for voiding`)],submit:{label:`Void voucher`,actionId:`vouchers.void:${a}`}})),h&&b.push(v({blockId:`act-extend`,fields:[g(`date`,`New expiry date`,{placeholder:`YYYY-MM-DD`}),g(`reason`,`Reason`)],submit:{label:`Extend expiry`,actionId:`vouchers.extend:${a}`}}),v({blockId:`act-redeem`,fields:[...c.kind===`money`?[g(`amount`,`Amount to take off (e.g. 12.50)`)]:[],g(`note`,`Note (what was it spent on?)`)],submit:{label:`Redeem by hand`,actionId:`vouchers.redeem:${a}`}}),v({blockId:`act-resend`,fields:[g(`to`,`Send to (email)`,{placeholder:c.delivery===`post`?`Leave empty to just show the code`:`Leave empty for the recipient, else the buyer`}),g(`reason`,`Reason`)],submit:{label:`Resend with a new code`,actionId:`vouchers.resend:${a}`}})),c.holds.length>0&&b.push(v({blockId:`act-release`,fields:[_(`rid`,`Hold`,c.holds.map(e=>({label:`${e.ref}: ${K(c,e.amountMinor)}`,value:e.rid})),{initialValue:c.holds[0]?.rid}),g(`reason`,`Reason`)],submit:{label:`Release hold`,actionId:`vouchers.release:${a}`}})),c.delivery===`post`&&!c.postedAt&&c.status===`active`&&b.push(p([m(`vouchers.posted`,`Mark as posted`,{value:a})])),b.length>0&&y.push(n(`Actions`),r(`Each action is written to the ledger with your name and the reason. A code is stored nowhere, so Resend makes a new one (the old code stops working).`),...b),y.push(n(`Ledger`),s({columns:[o(`at`,`When`),o(`type`,`What`,`badge`),o(`amount`,`Amount`),o(`by`,`By`),o(`note`,`Note`)],rows:[...c.ledger].reverse().slice(0,60).map(e=>({at:G(e.at),type:e.type,amount:e.amountMinor===0?``:K(c,e.amountMinor),by:e.by,note:[e.ref,e.note].filter(Boolean).join(`: `)})),pageActionId:`vouchers.ledger`,blockId:`voucher-ledger`}),p([m(`vouchers.list`,`Back to vouchers`,{style:`secondary`})])),y}async function J(e,n,r,i){let a=Gn.test(n)?await e.store.vouchers.get(n):null;return a?q(e,n,a,r,i):[t(`Voucher`),...f([W(`Voucher not found`)]),p([m(`vouchers.list`,`Back to vouchers`)])]}async function rr(e,t,n,r){let i=Gn.test(t)?await e.store.vouchers.get(t):null;if(!i)return J(e,t);let a=n(i);return a.ok?(await e.store.vouchers.updateIf(t,{where:{version:i.version},set:{...a.value}})).applied?q(e,t,a.value,U(r)):J(e,t,W(`Not done`,`The voucher changed while you were working: check it and try again.`)):q(e,t,i,W(`Not done`,a.error))}async function ir(e,t,n,r){let{vouchers:i,redemptions:a}=e.store;for(let o=0;o<2;o++){let o=Gn.test(t)?await i.get(t):null;if(!o)return J(e,t);let s=hn(o,n,{by:e.by,reason:r},e.now);if(!s.ok)return q(e,t,o,W(`Not released`,s.error));if(!(await a.updateIf(n,{where:{state:`held`},set:{state:`released`,settledAt:new Date(e.now).toISOString()}})).applied){let r=await a.get(n);if(r&&r.state!==`released`)return q(e,t,o,W(`Not released`,`That hold was already settled (${r.state}).`))}if((await i.updateIf(t,{where:{version:o.version},set:{...s.value}})).applied)return q(e,t,s.value,U(`Hold released`))}return J(e,t,W(`Not released`,`The voucher kept changing: try again.`))}const ar=async()=>{let e=Dt();return{code:e,id:await At(e),display:kt(e)}};function or(e,t,n,r,i){let a=en(e.ctx.site?.url),o=e.ctx.site?.name||`us`,s=new Date(t.expiresAt).toLocaleDateString(`en-GB`,{day:`numeric`,month:`long`,year:`numeric`,timeZone:`UTC`}),c=[`Hello,`,``,`Here is your gift voucher from ${o}.`,``,t.activity?`For: ${t.activity.label}`:`Value: ${K(t,t.balanceMinor)}`,`Code: ${r}`,`Valid until: ${s}`,...a?[`View or print it: ${a}/_vouchers/v?c=${r}`]:[],``,`Enter the code in the Gift voucher field when you book. Keep it safe: anyone who has it can use the voucher.`].join(`
3
+ `);return{id:`resend-${i.slice(0,24)}`,data:{message:{to:n,subject:`Your gift voucher from ${o}`,text:c,kind:`voucher_resend`},status:`pending`,attempts:0,nextAttemptAt:e.now,createdAt:e.now}}}const sr=(e,t)=>[...f([U(t?`The new code is on its way to ${t}`:`The new code`,`It is shown here once and stored nowhere. Copy it now.`)]),c({code:e})];async function cr(e,t,n,r){let{vouchers:i,outbox:a}=e.store,o=Gn.test(t)?await i.get(t):null;if(!o)return J(e,t);let s=await ar(),c=_n(o,{by:e.by,reason:r,oldId:t,newId:s.id,newCode:s.code},e.now);if(!c.ok)return q(e,t,o,W(`Not done`,c.error));if(n&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n))return q(e,t,o,W(`Not done`,`That email address does not look right.`));let l=n||(o.delivery===`post`?``:o.recipientEmail||o.buyerEmail);return(await i.updateIf(t,{where:{version:o.version},set:{...c.value.old}})).applied?(await i.compareAndSet(s.id,null,c.value.next),l&&await a.putMany([or(e,c.value.next,l,s.display,s.id)]),q(e,s.id,c.value.next,U(`Code replaced`),sr(s.display,l||null))):J(e,t,W(`Not done`,`The voucher changed while you were working: check it and try again.`))}async function lr(e,n,i){let a=Kt(await P(e.ctx.settings));return[t(`Issue a voucher`),...f([n]),...i?[...i.block,p([nr(i.id,`Open the voucher`)])]:[],r(`For a raffle prize, or a voucher paid for in the shop. It is flagged Not sold online and shows in the reports like any other. The code is shown once here (and emailed if you add an address).`),v({blockId:`issue-form`,fields:[_(`kind`,`Kind`,[{label:`Money`,value:`money`},{label:`Activity`,value:`activity`}],{initialValue:`money`}),g(`amount`,`Amount (${a.currency}, money vouchers)`,{placeholder:`25.00`}),_(`activity`,`Activity (activity vouchers)`,a.activityTypes.length>0?a.activityTypes.map((e,t)=>({label:`${e.label} (${k(e.priceMinor,a.currency)})`,value:String(t)})):[{label:`None set up`,value:`none`}]),g(`holderName`,`Who it is for (optional)`),g(`holderEmail`,`Email it to (optional)`),g(`months`,`Valid for (months, optional)`,{placeholder:String(a.validityMonths)}),g(`note`,`Note (why is it being issued?)`)],submit:{label:`Issue voucher`,actionId:`vouchers.issue`}})]}async function ur(e,t){let n=Kt(await P(e.ctx.settings)),r;if(H(t.amount))try{r=Ge(H(t.amount,20),n.currency)}catch{return lr(e,W(`Check the form`,`Enter the amount like 25.00.`))}let i=await ar(),a=vn({by:e.by,kind:H(t.kind,20),amountMinor:r,activityIndex:/^\d+$/.test(H(t.activity,4))?Number(H(t.activity,4)):void 0,holderName:H(t.holderName,120),holderEmail:H(t.holderEmail,200),note:H(t.note),validityMonths:H(t.months,4)?Number(H(t.months,4)):void 0},n,i.code,e.now);return a.ok?(await e.store.vouchers.compareAndSet(i.id,null,a.value),a.value.buyerEmail&&await e.store.outbox.putMany([or(e,a.value,a.value.buyerEmail,i.display,i.id)]),lr(e,U(`Voucher issued`),{block:sr(i.display,a.value.buyerEmail||null),id:i.id})):lr(e,W(`Check the form`,a.error))}async function dr(e,n){let i=$n(await e.store.vouchers.query({where:{delivery:`post`},orderBy:{expiresAt:`desc`},limit:50})).filter(({v:e})=>e.status===`active`&&!e.postedAt);return[t(`To post`),...f([n]),r(`Vouchers bought for posting. A code is stored nowhere, so open the voucher and use Resend (leave the email empty): it makes a new code and shows it to you once to print. Then mark the voucher as posted.`),s({columns:[o(`code`,`Code ends`,`code`),o(`value`,`Value`),o(`to`,`For`),o(`address`,`Address`),o(`expires`,`Expires`),o(`actions`,``,`element`)],rows:i.map(({id:e,v:t})=>({code:t.last4,value:Kn(t),to:t.recipientName||t.buyerName,address:t.address??``,expires:G(t.expiresAt),actions:m(`vouchers.open`,`Open`,{value:e})})),pageActionId:`vouchers.topost`,emptyText:`Nothing to post.`,blockId:`to-post`})]}async function Y(e,i={}){let a=Math.floor(e.now/1e3),{redemptions:c,vouchers:l}=e.store,[u,d]=await Promise.all([c.query({where:{lateOpen:!0},limit:25}),c.query({where:{state:`held`,createdAt:{lte:new Date(e.now-31*6e4).toISOString()}},orderBy:{createdAt:`asc`},limit:25})]),p=u.items.map(e=>({id:e.id,r:e.data})),h=d.items.map(e=>({id:e.id,r:e.data})).filter(({r:e})=>a>e.exp+60),_=[...new Set([...p,...h].map(e=>e.r.voucherId))],y=_.length>0?await l.getMany(_):new Map,b=e=>y.get(e)?.last4??`?`,x=e=>k(e.amountMinor,e.currency);return[t(`Unsettled`),...f([i.note]),r(`Redemptions Events did not finish settling. A hold past its window gives its money back when you release it on the voucher. A late outcome arrived after the hold had been decided: look at the booking, fix the voucher with Redeem by hand if needed, then mark it reviewed.`),...i.ask?[v({blockId:`review-form`,fields:[g(`note`,`What did you do about it?`)],submit:{label:`Mark reviewed`,actionId:`unsettled.review:${i.ask}`}})]:[],n(`Late outcomes`),s({columns:[o(`code`,`Code ends`,`code`),o(`ref`,`Booking`),o(`state`,`Hold`,`badge`),o(`late`,`Arrived late`),o(`actions`,``,`element`)],rows:p.map(({id:e,r:t})=>({code:b(t.voucherId),ref:t.ref,state:t.state,late:t.late.map(e=>`${e.outcome}${e.creditMinor?` ${k(e.creditMinor,t.currency)}`:``}`).join(`, `),actions:m(`unsettled.review_ask`,`Mark reviewed`,{value:e})})),pageActionId:`unsettled.late`,emptyText:`No late outcomes.`,blockId:`unsettled-late`}),n(`Holds past their window`),s({columns:[o(`code`,`Code ends`,`code`),o(`ref`,`Booking`),o(`amount`,`Held`),o(`ended`,`Window ended`),o(`actions`,``,`element`)],rows:h.map(({r:e})=>({code:b(e.voucherId),ref:e.ref,amount:e.kind===`activity`?`One use`:x(e),ended:G(new Date(e.exp*1e3).toISOString()),actions:m(`vouchers.open`,`Open to release`,{value:e.voucherId})})),pageActionId:`unsettled.held`,emptyText:`No holds past their window.`,blockId:`unsettled-held`})]}async function fr(e,t,n){let{redemptions:r,vouchers:i}=e.store,a=await r.get(t);if(!a?.lateOpen)return Y(e,{note:W(`Nothing to review`,`That outcome is already reviewed.`)});if(!n)return Y(e,{ask:t,note:W(`Add a note`,`Say what you did about it.`)});let o=await i.get(a.voucherId);if(o){let r={at:new Date(e.now).toISOString(),type:`note`,amountMinor:0,by:e.by,rid:t,ref:a.ref,note:`Reviewed late ${a.late.map(e=>e.outcome).join(`, `)}: ${n}`};await i.updateIf(a.voucherId,{where:{version:o.version},set:{version:o.version+1,ledger:[...o.ledger,r]}})}return await r.updateIf(t,{where:{lateOpen:!0},set:{lateOpen:!1}}),Y(e,{note:U(`Marked reviewed`)})}async function pr(e,n,r){let{ctx:i,store:a}=e,[l,u]=await Promise.all([a.clients.query({orderBy:{createdAt:`desc`},limit:10}),i.settings.get(`pairingClientId`)]),d=l.items.map(e=>({id:e.id,c:e.data})),h=d.some(e=>e.id===u&&!e.c.revokedAt);return[t(`Pairing with Events`),...f([n,h?U(`Events is paired`,`Active client: ${String(u)}.`):{severity:`warning`,title:`Events is not paired`,message:`Redeeming in bookings is off until you pair.`}]),...r?[...f([U(`Your pairing string`,`Paste it into Events once. It is shown here once and stored nowhere. Pairing again makes a new client; revoke the old one when Events uses the new string.`)]),c({code:r.pairing})]:[],p([m(`pairing.create`,h?`Pair again (new string)`:`Pair Events`,{style:`primary`,confirm:{title:`Make a new pairing string?`,text:`Events needs the new string to redeem vouchers.`,confirm:`Make string`,deny:`Cancel`}})]),s({columns:[o(`id`,`Client`,`code`),o(`created`,`Created`),o(`status`,`Status`,`badge`),o(`action`,``,`element`)],rows:d.map(({id:e,c:t})=>({id:e,created:G(t.createdAt),status:t.revokedAt?`revoked`:e===u?`active`:`older`,action:t.revokedAt?``:m(`pairing.revoke`,`Revoke`,{style:`danger`,value:e,confirm:{title:`Revoke this client?`,text:`Its bookings can no longer settle.`,confirm:`Revoke`,deny:`Cancel`}})})),pageActionId:`pairing.clients`,emptyText:`Not paired yet.`,blockId:`pairing-clients`})]}async function mr(e){let t=[],n,r=!1;for(let i=0;i<8;i++){let i=await e.store.vouchers.query({orderBy:{expiresAt:`asc`},limit:100,...n?{cursor:n}:{}});for(let e of i.items)t.push(e.data);if(r=!!i.hasMore&&!!i.cursor,!r)break;n=i.cursor}return{list:t,more:r}}const hr=(e,t)=>`${en(e.ctx.site?.url)}/_emdash/api/plugins/semics-vouchers/reports/export?report=${t}`;function gr(e,t,n){let r=[...new Set(e.filter(e=>e.kind===t).map(e=>`${e.month}|${e.currency}`))],i=(n,r,i)=>e.find(e=>e.kind===t&&e.month===n&&e.currency===r&&e.move===i);return s({columns:[o(`month`,`Month`),...xn.map(e=>o(e,{sold:`Sold`,redeemed:`Redeemed`,expired:`Expired (breakage)`,voided:`Voided`,credited:`Credited back`}[e]))],rows:r.map(e=>{let[n=``,r=`GBP`]=e.split(`|`),a={month:n};for(let e of xn){let o=i(n,r,e);a[e]=o?t===`money`?k(o.minor,r):`${o.count} (${k(o.minor,r)} paid)`:`-`}return a}),pageActionId:`reports.${n}`,emptyText:t===`money`?`No money voucher movements yet.`:`No activity voucher movements yet.`,blockId:`movements-${n}`})}async function _r(e){let{list:i,more:o}=await mr(e),s=Tn(i,e.now),c=Dn(i,e.now);return[t(`Voucher reports`),...f([o&&{severity:`warning`,title:`Showing the first 800 vouchers`,message:`Download the CSV for a part of the list, or ask for a fuller report.`}]),r(`Money and activity vouchers are never added together. These reports carry no tax figures.`),n(`Outstanding liability: money`),a(s.money.length===0?[{label:`Unspent money`,value:k(0,`GBP`)}]:s.money.flatMap(e=>[{label:`Unspent money (${e.currency})`,value:k(e.balanceMinor,e.currency),description:`${e.count} voucher${e.count===1?``:`s`}`},{label:`Held by bookings (${e.currency})`,value:k(e.heldMinor,e.currency)}])),n(`Outstanding liability: activity vouchers`),a(s.activity.length===0?[{label:`Activity vouchers to use`,value:0}]:s.activity.map(e=>({label:`Activity vouchers to use (${e.currency})`,value:e.count,description:`${k(e.paidMinor,e.currency)} paid for them`}))),p([h(`Download liability CSV`,{kind:`external`,url:hr(e,`liability`)}),h(`Download movements CSV`,{kind:`external`,url:hr(e,`movements`)})]),n(`Movements by month: money`),gr(c,`money`,`money`),n(`Movements by month: activity vouchers`),gr(c,`activity`,`activity`),r(`Breakage is what was left on a voucher when it expired, counted in the month it expired.`)]}const vr=(e,t,n)=>Rt({status:e,headers:{"content-type":n?`text/csv; charset=utf-8`:`text/plain; charset=utf-8`,...n?{"content-disposition":`attachment; filename="${n}"`}:{}},body:{kind:`text`,value:t}}),yr={methods:[`GET`],response:`raw`,handler:(async(e,t)=>{let n=e.input?.report;if(n!==`liability`&&n!==`movements`)return vr(400,`Unknown report.`);let r=br(e,t),{list:i}=await mr(r);return vr(200,n===`liability`?Nn(i,r.now):Mn(Dn(i,r.now)),`vouchers-${n}.csv`)})};function br(e,t){return{ctx:t,store:t.storage,by:un(e.user),now:Date.now()}}const xr=e=>({q:H(e.q,100),kind:Qn(e.kind),status:Qn(e.status),expiring:Qn(e.expiring)===`30`});async function Sr(e,n){if(n.type===`page_load`)switch(n.page){case`/vouchers`:return tr(e,{});case`/to-post`:return dr(e);case`/unsettled`:return Y(e);case`/reports`:return _r(e);case`/pairing`:return pr(e);case`/issue`:return lr(e);default:return[t(`Vouchers`),...f([W(`Page not found`)])]}let[r=``,i=``]=n.action_id.split(/:(.*)/s);if(n.type===`form_submit`){let t=n.values;switch(r){case`vouchers.search`:return tr(e,xr(t));case`vouchers.void`:return rr(e,i,n=>fn(n,{by:e.by,reason:H(t.reason)},e.now),`Voucher voided`);case`vouchers.extend`:return rr(e,i,n=>pn(n,{by:e.by,reason:H(t.reason),date:H(t.date,10)},e.now),`Expiry extended`);case`vouchers.redeem`:return rr(e,i,n=>{let r;if(n.kind===`money`)try{r=Ge(H(t.amount,20),n.currency)}catch{return C(`Enter the amount like 12.50.`)}return mn(n,{by:e.by,note:H(t.note),amountMinor:r},e.now)},`Redeemed by hand`);case`vouchers.resend`:return cr(e,i,H(t.to,200),H(t.reason));case`vouchers.release`:return ir(e,i,H(t.rid,64),H(t.reason));case`vouchers.issue`:return ur(e,t);case`unsettled.review`:return fr(e,i,H(t.note))}}else{let t=n.value;switch(r){case`vouchers.list`:return tr(e,{});case`vouchers.page`:{let[n,r,a]=i.split(`|`);return tr(e,{kind:n||void 0,status:r||void 0,expiring:a===`30`,cursor:H(t,500)})}case`vouchers.open`:return J(e,H(t,64));case`vouchers.posted`:return rr(e,H(t,64),t=>gn(t,{by:e.by},e.now),`Marked as posted`);case`unsettled.review_ask`:return Y(e,{ask:H(t,64)});case`pairing.create`:{let t=await Wn[`pairing/create`].handler({},e.ctx);return pr(e,U(`Paired`),{pairing:t.pairing})}case`pairing.revoke`:return pr(e,(await Wn[`pairing/revoke`].handler({input:{clientId:H(t,64)}},e.ctx)).ok?U(`Client revoked`):W(`Could not revoke`,`It may already be revoked.`))}}return[t(`Vouchers`),...f([W(`Page not found`)])]}const Cr={handler:(async(e,t)=>({blocks:await Sr(br(e,t),e.input)}))},wr=`outbox-drain`,Tr=`vouchers-daily`,Er=[{name:wr,schedule:`* * * * *`},{name:Tr,schedule:`15 3 * * *`}];function Dr(e){let t=e.storage.outbox;return{put:(e,n)=>t.put(e,n),putMany:e=>t.putMany([...e]),query:({status:e,nextAttemptAtLte:n,limit:r})=>t.query({where:{status:e,...n===void 0?{}:{nextAttemptAt:{lte:n}}},orderBy:{nextAttemptAt:`asc`},limit:r})}}async function Or(e,t){let n=e.email;await Je({storage:Dr(e),email:n?{send:e=>n.send({to:e.to,subject:e.subject,text:e.text})}:void 0,now:t})}async function kr(e,t){let n=e.storage.vouchers,{items:r}=await n.query({where:{status:`active`,expiresAt:{lte:new Date(t).toISOString()}},limit:5});for(let{id:e,data:t}of r){let r=t;await n.updateIf(e,{where:{version:r.version,status:`active`},set:{status:`expired`,version:r.version+1}})}await Qe(e.kv,t,3)}async function Ar(e,t,n=Date.now()){if(e.name===wr)await Or(t,n);else if(e.name===Tr)await kr(t,n);else return!1;return!0}const jr=/^[^\s@<>()",;:\\[\]]{1,64}@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/,Mr=/^\d{4}-\d{2}-\d{2}$/,Nr=864e5;function X(e,t){if(!j(e))return;let n=e.trim();return n.length>=1&&n.length<=t&&!/[\u0000-\u0008\u000b-\u001f\u007f]/.test(n)?n:void 0}const Pr=e=>{let t=X(e,254);return t&&jr.test(t)?t.toLowerCase():void 0};function Fr(e){if(!j(e)||!Mr.test(e))return;let t=Date.parse(`${e}T00:00:00.000Z`);return Number.isFinite(t)&&new Date(t).toISOString().startsWith(e)?t:void 0}function Ir(e,t,n){let r=C(`BAD_REQUEST`);if(!e||typeof e!=`object`)return r;let i=e,a=i.kind;if(a!==`money`&&a!==`activity`||!t.kinds.includes(a))return r;let o,s=null;if(a===`money`){let e=i.amountMinor,n=M(e,1,A)&&t.moneyAmounts.includes(e),a=t.moneyCustom!==null&&M(e,t.moneyCustom.min,t.moneyCustom.max);if(!n&&!a)return r;o=e}else{let e=i.activity??{},n=t.activityTypes.find(t=>t.eventId===e.eventId&&t.ticketTypeId===e.ticketTypeId);if(!n)return r;o=n.priceMinor,s={eventId:n.eventId,ticketTypeId:n.ticketTypeId,label:n.label}}let c=i.buyer??{},l={name:X(c.name,120),email:Pr(c.email)};if(!l.name||!l.email)return r;let u=null;if(i.recipient!==void 0&&i.recipient!==null){let e=i.recipient,t=e.name===void 0||e.name===``?void 0:X(e.name,120),n=e.email===void 0||e.email===``?void 0:Pr(e.email);if(e.name!==void 0&&e.name!==``&&!t||e.email!==void 0&&e.email!==``&&!n)return r;u={...t?{name:t}:{},...n?{email:n}:{}}}let d=i.message===void 0||i.message===``?null:X(i.message,500)??!1;if(d===!1)return r;let f=i.delivery??`email-buyer`;if(!t.delivery.includes(f)||f===`email-recipient`&&!u?.email)return r;let p=null;if(f===`post`&&(p=X(i.address,500)??null,!p))return r;let m=null;if(i.sendOn!==void 0&&i.sendOn!==null&&i.sendOn!==``){let e=Fr(i.sendOn),a=Math.floor(n/Nr)*Nr;if(!t.scheduledSend||f!==`email-recipient`||e===void 0||e<a||e>a+365*Nr)return r;m=i.sendOn}return S({kind:a,amountMinor:o,currency:t.currency,activity:s,buyer:{name:l.name,email:l.email},recipient:u,message:d,delivery:f,address:p,sendOn:m,validityMonths:t.validityMonths})}const Lr=/^\/(?!\/)[A-Za-z0-9/_.-]{0,200}$/,Rr=()=>`vo_${x(crypto.getRandomValues(new Uint8Array(16)))}`;function zr(e){return e.activity?`Gift voucher: ${e.activity.label}`:`Gift voucher ${We(e.amountMinor,e.currency)} ${e.currency}`}function Br(e,t,n,r){let i=`${n}?order=${e}`;return new URLSearchParams([[`mode`,`payment`],[`client_reference_id`,e],[`customer_email`,t.buyer.email],[`expires_at`,String(r)],[`success_url`,i],[`cancel_url`,`${i}&cancelled=1`],[`metadata[plugin]`,`vouchers`],[`metadata[order]`,e],[`line_items[0][quantity]`,`1`],[`line_items[0][price_data][currency]`,t.currency.toLowerCase()],[`line_items[0][price_data][unit_amount]`,String(t.amountMinor)],[`line_items[0][price_data][product_data][name]`,zr(t)],[`payment_intent_data[metadata][plugin]`,`vouchers`],[`payment_intent_data[metadata][order]`,e]])}function Vr(e,t){try{return O(t)!==3||e%10==0}catch{return!1}}const Hr=async(e,t)=>{let n=Date.now(),r=await P(t.settings),i=Ir(e.input,Kt(r),n);if(!i.ok||!Vr(i.value.amountMinor,i.value.currency))return N(`BAD_REQUEST`);let a=e.input,o=typeof a.returnPath==`string`?a.returnPath:`/_vouchers/thanks`;if(!Lr.test(o))return N(`BAD_REQUEST`);let s=await Qt(r,t.site?.url);if(s)return N(s);let c=e.requestMeta?.ip??null,l=typeof r.turnstileSecretKey==`string`?r.turnstileSecretKey:``,u=$t(t),[d,f]=await Promise.all([F(t.kv,t.settings,r,`purchase`,c,n),l?ot(u,{secret:l,token:typeof a.turnstileToken==`string`?a.turnstileToken:``,...c?{remoteIp:c}:{}}):Promise.resolve({ok:!0})]);if(d)return N(`RATE_LIMITED`);if(!f.ok)return N(`TURNSTILE_FAILED`);let p=r.stripeSecretKey,m=en(t.site?.url);if(typeof p!=`string`||!p||!m||!t.http)return N(`PAYMENT_FAILED`);let h=Rr(),g={status:`pending`,...i.value,createdAt:new Date(n).toISOString()};await t.storage.orders.put(h,g);let _=Br(h,i.value,`${m}${o}`,Math.floor(n/1e3)+1860);try{let e=await Ft(t.http,p,`checkout/sessions`,_,`voucher-${h}`),n=await e.json(),r=typeof n.url==`string`?n.url:``;return!e.ok||typeof n.id!=`string`||!r.startsWith(`https://`)?N(`PAYMENT_FAILED`):{ok:!0,orderId:h,url:r}}catch{return N(`PAYMENT_FAILED`)}},Ur=/^[A-Za-z0-9_-]{1,64}$/,Wr=e=>typeof e==`string`&&Ur.test(e);function Gr(e){let t=e??{},n={};if(t.currency!==void 0){if(typeof t.currency!=`string`||!/^[A-Z]{3}$/.test(t.currency))return null;n.currency=t.currency}if(t.lines!==void 0){if(!Array.isArray(t.lines)||t.lines.length>50)return null;n.lines=[];for(let e of t.lines){let{eventId:t,ticketTypeId:r}=e??{};if(!Wr(t)||!Wr(r))return null;n.lines.push({eventId:t,ticketTypeId:r})}}return n}const Kr=async(e,t)=>{let n=Date.now(),r=Gr(e.input);if(!r)return N(`BAD_REQUEST`);let i=await P(t.settings);if(await F(t.kv,t.settings,i,`check`,e.requestMeta?.ip??null,n))return N(`RATE_LIMITED`);if(r.lines&&!await Bn(i))return N(`VOUCHER_UNAVAILABLE`);let a=await At(e.input?.code),o=t.storage.vouchers,s=a?await o.get(a):null;if(!s)return N(`VOUCHER_INVALID`);let c=on(s,r,n);return c.ok?{ok:!0,voucher:c.value}:N(c.error)},qr=/^[A-Za-z0-9._:-]{1,128}$/,Jr=/^[A-Za-z0-9_-]{1,64}$/,Yr=(e,t=0)=>typeof e==`number`&&Number.isSafeInteger(e)&&e>=t&&e<=lt;function Xr(e){if(!e||typeof e!=`object`)return null;let t=e;if(typeof t.ref!=`string`||!qr.test(t.ref)||!Yr(t.dueMinor)||typeof t.currency!=`string`||!/^[A-Z]{3}$/.test(t.currency)||!Array.isArray(t.lines)||t.lines.length>50)return null;let n=[];for(let e of t.lines){let t=e??{};if(typeof t.eventId!=`string`||!Jr.test(t.eventId)||typeof t.ticketTypeId!=`string`||!Jr.test(t.ticketTypeId)||!Yr(t.unitMinor)||!Yr(t.qty,1)||t.qty>1e3)return null;n.push({eventId:t.eventId,ticketTypeId:t.ticketTypeId,unitMinor:t.unitMinor,qty:t.qty})}return{code:t.code,ref:t.ref,dueMinor:t.dueMinor,currency:t.currency,lines:n}}function Zr(e,t,n,r){return{v:1,typ:`hold`,cid:e,rid:t.rid,iat:t.iat,exp:t.exp,ref:t.ref,kind:n.kind,amountMinor:t.amountMinor,currency:n.currency,...r?{line:r}:{}}}async function Qr(e,t,n){let r=Math.floor(n/1e3),i=e.holds.slice(0,5);return Promise.all(i.map(e=>xt(`lookup`,{v:1,typ:`lookup`,cid:t.cid,ts:r,ref:e.ref,rid:e.rid},t.secret)))}async function $r(e,t,n){if(!e.client)return{ok:!1,code:`VOUCHER_UNAVAILABLE`};let r=e.newRid();for(let i=0;i<3;i++){let i=await e.vouchers.get(t);if(!i)return{ok:!1,code:`VOUCHER_INVALID`};let a=sn(i,n,e.now,r);if(!a.ok)return a.error!==`VOUCHER_NO_BALANCE`||i.holds.length===0?{ok:!1,code:a.error}:{ok:!1,code:a.error,lookups:await Qr(i,e.client,e.now)};let{hold:o,line:s}=a.value;if(!a.value.reused){let{next:n}=a.value,c={version:i.version};i.kind===`money`?c.balanceMinor={gte:o.amountMinor}:c.usesLeft=1;let{version:l,balanceMinor:u,usesLeft:d,heldMinor:f,holds:p,ledger:m,status:h}=n;if(!(await e.vouchers.updateIf(t,{where:c,set:{version:l,balanceMinor:u,usesLeft:d,heldMinor:f,holds:p,ledger:m,status:h}})).applied)continue;await e.redemptions.put(r,{voucherId:t,cid:e.client.cid,ref:o.ref,kind:i.kind,amountMinor:o.amountMinor,currency:i.currency,line:s??null,iat:o.iat,exp:o.exp,state:`held`,creditedMinor:0,createdAt:new Date(e.now).toISOString(),late:[]})}return{ok:!0,hold:await xt(`hold`,Zr(e.client.cid,o,i,s),e.client.secret)}}return{ok:!1,code:`VOUCHER_UNAVAILABLE`}}const ei=async(e,t)=>{let n=Date.now(),r=Xr(e.input);if(!r)return N(`BAD_REQUEST`);let i=await P(t.settings);if(await F(t.kv,t.settings,i,`hold`,e.requestMeta?.ip??null,n))return N(`RATE_LIMITED`);let a=await Bn(i);if(!a)return N(`VOUCHER_UNAVAILABLE`);let o=await At(r.code);if(!o)return N(`VOUCHER_INVALID`);let s=t.storage,c=await $r({vouchers:s.vouchers,redemptions:s.redemptions,now:n,newRid:()=>`rd_${ge().slice(0,32)}`,client:a},o,r);return c.ok?{ok:!0,hold:c.hold}:N(c.code,c.lookups?{lookups:c.lookups}:{})},ti=async(e,t)=>{let n=Date.now(),r=Math.floor(n/1e3),i=await P(t.settings);if(await F(t.kv,t.settings,i,`settle`,e.requestMeta?.ip??null,n))return N(`RATE_LIMITED`);let a=t.storage,o=e.input?.settle,s=await yt(`settle`,{token:o,now:n,getClient:Vn(i,a.clients)});if(!s.ok)return N(s.error===`CLIENT_UNKNOWN`||s.error===`CLIENT_REVOKED`?`TOKEN_INVALID`:s.error);let c=s.value,l=await a.redemptions.get(c.rid);if(!l||l.ref!==c.ref||l.cid!==c.cid)return N(`REF_UNKNOWN`);let u=cn(l,c,r);if(u.next){let{state:e,creditedMinor:t,late:r}=u.next,i=await a.redemptions.updateIf(c.rid,{where:{state:l.state,creditedMinor:l.creditedMinor},set:{state:e,creditedMinor:t,late:r,...e===l.state?{}:{settledAt:new Date(n).toISOString()},...r.length>l.late.length?{lateOpen:!0}:{}}});if(!i.applied)return{ok:!0,state:(await a.redemptions.get(c.rid))?.state??l.state};l=i.data}if(l.state!==`held`)for(let e=0;e<2;e++){let e=await a.vouchers.get(l.voucherId),t=e&&ln(e,c.rid,l,n);if(!e||!t)break;let{version:r,balanceMinor:i,usesLeft:o,heldMinor:s,holds:u,ledger:d,status:f}=t;if((await a.vouchers.updateIf(l.voucherId,{where:{version:e.version},set:{version:r,balanceMinor:i,usesLeft:o,heldMinor:s,holds:u,ledger:d,status:f}})).applied)break}return{ok:!0,state:l.state}},ni=e=>new Date(e).toLocaleDateString(`en-GB`,{day:`numeric`,month:`long`,year:`numeric`,timeZone:`UTC`});function ri(e){return e.activity?`For: ${e.activity.label}`:`Value: ${k(e.amountMinor,e.currency)}`}function ii(e,t,n,r){return[ri(e),`Code: ${t}`,`Valid until: ${ni(r)}`,...n?[`View or print it: ${n}`]:[],``,`Enter the code in the Gift voucher field when you book. Keep it safe: anyone who has it can use the voucher.`]}const ai=(e,t,n,r,i,a)=>({message:{to:e,subject:t,text:n,kind:r},status:`pending`,attempts:0,nextAttemptAt:i,createdAt:a});function oi({orderId:e,order:t,code:n,origin:r,site:i,now:a}){let o=kt(n),s=r?`${r}/_vouchers/v?c=${o}`:``,c=tn(a,t.validityMonths),l=i||`us`,u=t.delivery===`email-recipient`&&t.recipient?.email?t.recipient.email:null,d=t.sendOn?Math.max(a,Date.parse(`${t.sendOn}T${`7`.padStart(2,`0`)}:00:00.000Z`)):a,f=u?t.sendOn&&d>a?`We will email it to ${u} on ${ni(d)}.`:`We have emailed it to ${u}.`:t.delivery===`post`?`We will post it to the address you gave.`:`Print it from the link above, or forward this email.`,p=[`Hello ${t.buyer.name},`,``,`Thank you for buying a gift voucher from ${l}.`,``,...ii(t,o,s,c),``,f].join(`
4
+ `),m=[{id:`receipt-${e}`,data:ai(t.buyer.email,`Your gift voucher from ${l}`,p,`voucher_receipt`,a,a)}];if(u){let n=[`Hello${t.recipient?.name?` ${t.recipient.name}`:``},`,``,`${t.buyer.name} has sent you a gift voucher for ${l}.`,...t.message?[``,t.message]:[],``,...ii(t,o,s,c)].join(`
5
+ `);m.push({id:`gift-${e}`,data:ai(u,`${t.buyer.name} sent you a gift voucher`,n,`voucher_gift`,d,a)})}return m}const si=/^vo_[0-9a-f]{32}$/,Z=e=>typeof e==`string`&&e.length>0&&e.length<=255?e:null;function ci(e){let t;try{t=JSON.parse(e)}catch{return null}let n=t?.data?.object,r=Z(t?.id),i=Z(t?.type),a=Z(n?.id);if(!r||!i||!n||!a)return null;let o=n.metadata??{},s=n.payment_intent;return{id:r,type:i,sessionId:a,paymentStatus:Z(n.payment_status),paymentIntent:Z(s)??Z(s?.id),amountTotal:Number.isSafeInteger(n.amount_total)?n.amount_total:null,currency:Z(n.currency),plugin:Z(o.plugin),orderId:Z(o.order)}}const Q=(e,t)=>Rt({status:e,headers:{"content-type":`application/json; charset=utf-8`},body:{kind:`text`,value:JSON.stringify(t)}});async function li(e,t,n,r,i){let a=r.pendingCode,o=r.voucherId;await t.vouchers.compareAndSet(o,null,nn(r,{orderId:n,code:a},i));let s=oi({orderId:n,order:r,code:a,origin:en(e.site?.url),site:e.site?.name??``,now:i});await t.outbox.putMany(s),await t.orders.updateIf(n,{where:{status:`paid`,voucherId:o},set:{pendingCode:null,issuedAt:new Date(i).toISOString()}})}async function ui(e,t,n,r,i){let a=Dt(),o=await At(a),s=await t.orders.updateIf(r,{where:{status:`pending`,amountMinor:n.amountTotal??-1,currency:(n.currency??``).toUpperCase()},set:{status:`paid`,sessionId:n.sessionId,paidAt:new Date(i).toISOString(),paidEventId:n.id,paymentIntent:n.paymentIntent,voucherId:o,pendingCode:a}});if(s.applied)return li(e,t,r,s.data,i);let c=await t.orders.get(r);if(c){if(c.status===`paid`&&c.pendingCode)return li(e,t,r,c,i);c.status===`pending`&&(await t.orders.updateIf(r,{where:{status:`pending`},set:{status:`mismatch`,sessionId:n.sessionId,paidEventId:n.id,paymentIntent:n.paymentIntent}})).applied&&await e.log.warn(`Vouchers: a payment did not match its order and no voucher was issued; refund it in Stripe`,{orderId:r,sessionId:n.sessionId})}}async function di(e,t,n,r,i){await e.orders.updateIf(n,{where:{status:`pending`},set:{status:r,sessionId:t.sessionId,closedAt:new Date(i).toISOString()}})}async function fi(e,t,n,r){let i=(await P(e.settings)).stripeWebhookSecret;if(typeof i!=`string`||!i)return Q(503,{error:`WEBHOOK_NOT_CONFIGURED`});if(typeof t!=`string`)return Q(400,{error:`BAD_REQUEST`});let a=await Lt(t,n,i,Math.floor(r/1e3));if(!a.ok)return Q(400,{error:a.error});let o=ci(t),s=o?.orderId;if(o?.plugin!==`vouchers`||!s||!si.test(s))return Q(200,{received:!0,ignored:!0});let c=e.storage;try{switch(o.type){case`checkout.session.completed`:o.paymentStatus===`paid`&&await ui(e,c,o,s,r);break;case`checkout.session.async_payment_succeeded`:await ui(e,c,o,s,r);break;case`checkout.session.expired`:await di(c,o,s,`expired`,r);break;case`checkout.session.async_payment_failed`:await di(c,o,s,`failed`,r);break;default:return Q(200,{received:!0,ignored:!0})}}catch{return Q(500,{error:`INTERNAL`})}return Q(200,{received:!0})}const pi={methods:[`POST`],public:!0,response:`raw`,request:{body:`text`,headers:[`stripe-signature`]},handler:((e,t)=>fi(t,e.input,e.request?.headers?.[`stripe-signature`],Date.now()))},mi=Pt(Yt,{devKey:!1}),$=e=>({methods:[`POST`],public:!0,handler:e}),hi={hooks:{"plugin:activate":async(e,t)=>{await nt(t,Er)},cron:async(e,t)=>{await it(e,t,Er,Date.now())||await Ar(e,t)}},routes:{"public/purchase":at($(Hr)),"public/redeem/hold":$(ei),"public/redeem/settle":$(ti),"public/redeem/check":$(Kr),"public/options":$(async(e,t)=>{let n=await P(t.settings);return{ok:!0,...Jt(Kt(n),n)}}),"webhooks/stripe":pi,...Wn,admin:Cr,"reports/export":yr,"pro/licence":mi.statusRoute()}};export{hi as default,mi as licence};
@@ -0,0 +1,205 @@
1
+ // Generated by scripts/generate.mjs from manifest.jsonc + pro.json. Do not edit.
2
+ {
3
+ "$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
4
+ "slug": "semics-vouchers",
5
+ "publisher": "did:plc:mire6xkehfirmoj5pmpqgpvq",
6
+ "license": "LicenseRef-Semics-Pro",
7
+ "author": {
8
+ "name": "semics",
9
+ "url": "https://semics.co.uk"
10
+ },
11
+ "security": {
12
+ "email": "security@semics.co.uk"
13
+ },
14
+ "description": "Gift vouchers for EmDash: sold online through Stripe, redeemed in Events bookings",
15
+ "release": {
16
+ "requires": {
17
+ "env:emdash": ">=1.2.0 <2"
18
+ }
19
+ },
20
+ "capabilities": [
21
+ "email:send",
22
+ "network:request"
23
+ ],
24
+ "allowedHosts": [
25
+ "api.stripe.com",
26
+ "challenges.cloudflare.com"
27
+ ],
28
+ "storage": {
29
+ "vouchers": {
30
+ "indexes": [
31
+ "status",
32
+ "kind",
33
+ "expiresAt",
34
+ "last4",
35
+ "orderId",
36
+ "buyerEmail",
37
+ "recipientEmail",
38
+ "delivery",
39
+ [
40
+ "status",
41
+ "expiresAt"
42
+ ]
43
+ ]
44
+ },
45
+ "orders": {
46
+ "indexes": [
47
+ "status",
48
+ "createdAt",
49
+ "sessionId"
50
+ ]
51
+ },
52
+ "redemptions": {
53
+ "indexes": [
54
+ "voucherId",
55
+ "ref",
56
+ "state",
57
+ "lateOpen",
58
+ [
59
+ "state",
60
+ "createdAt"
61
+ ]
62
+ ]
63
+ },
64
+ "clients": {
65
+ "indexes": [
66
+ "createdAt"
67
+ ]
68
+ },
69
+ "outbox": {
70
+ "indexes": [
71
+ [
72
+ "status",
73
+ "nextAttemptAt"
74
+ ]
75
+ ]
76
+ }
77
+ },
78
+ "admin": {
79
+ "pages": [
80
+ {
81
+ "path": "/vouchers",
82
+ "label": "Vouchers",
83
+ "icon": "ticket"
84
+ },
85
+ {
86
+ "path": "/to-post",
87
+ "label": "To post",
88
+ "icon": "mail"
89
+ },
90
+ {
91
+ "path": "/unsettled",
92
+ "label": "Unsettled",
93
+ "icon": "calendar"
94
+ },
95
+ {
96
+ "path": "/reports",
97
+ "label": "Voucher reports",
98
+ "icon": "chart"
99
+ },
100
+ {
101
+ "path": "/issue",
102
+ "label": "Issue a voucher",
103
+ "icon": "tag"
104
+ },
105
+ {
106
+ "path": "/pairing",
107
+ "label": "Pairing",
108
+ "icon": "gear"
109
+ }
110
+ ],
111
+ "settingsSchema": {
112
+ "kinds": {
113
+ "type": "string",
114
+ "label": "Kinds of voucher sold",
115
+ "description": "money, activity, or both (comma-separated).",
116
+ "default": "money,activity"
117
+ },
118
+ "validityMonths": {
119
+ "type": "number",
120
+ "label": "Valid for (months)",
121
+ "description": "Applies to vouchers sold from now on. Existing vouchers keep their own expiry.",
122
+ "default": 12,
123
+ "min": 1,
124
+ "max": 60
125
+ },
126
+ "moneyAmounts": {
127
+ "type": "string",
128
+ "label": "Preset amounts (pence)",
129
+ "description": "Comma-separated, e.g. 2500,5000,10000 for £25, £50 and £100.",
130
+ "default": "2500,5000,10000"
131
+ },
132
+ "moneyCustom": {
133
+ "type": "boolean",
134
+ "label": "Buyers may choose another amount",
135
+ "default": true
136
+ },
137
+ "moneyCustomMin": {
138
+ "type": "number",
139
+ "label": "Smallest chosen amount (pence)",
140
+ "default": 1000,
141
+ "min": 100,
142
+ "max": 10000000
143
+ },
144
+ "moneyCustomMax": {
145
+ "type": "number",
146
+ "label": "Largest chosen amount (pence)",
147
+ "default": 50000,
148
+ "min": 100,
149
+ "max": 10000000
150
+ },
151
+ "activityTypes": {
152
+ "type": "string",
153
+ "multiline": true,
154
+ "label": "Activity vouchers",
155
+ "description": "One per line: event id, ticket type id, price in pence, label. For example: ev_123, tt_456, 3500, One clay session.",
156
+ "default": ""
157
+ },
158
+ "delivery": {
159
+ "type": "string",
160
+ "label": "Delivery options",
161
+ "description": "Any of email-buyer, email-recipient, print, post (comma-separated).",
162
+ "default": "email-buyer,email-recipient,print"
163
+ },
164
+ "scheduledSend": {
165
+ "type": "boolean",
166
+ "label": "Buyers may pick the day the recipient's email goes out",
167
+ "default": true
168
+ },
169
+ "currency": {
170
+ "type": "string",
171
+ "label": "Currency",
172
+ "description": "ISO code, e.g. GBP. One currency per site.",
173
+ "default": "GBP"
174
+ },
175
+ "turnstileSiteKey": {
176
+ "type": "string",
177
+ "label": "Turnstile site key"
178
+ },
179
+ "turnstileSecretKey": {
180
+ "type": "secret",
181
+ "label": "Turnstile secret key"
182
+ },
183
+ "stripeSecretKey": {
184
+ "type": "secret",
185
+ "label": "Stripe secret key",
186
+ "description": "Ideally a restricted key with write access to Checkout Sessions and Refunds. Stored encrypted."
187
+ },
188
+ "stripeWebhookSecret": {
189
+ "type": "secret",
190
+ "label": "Stripe webhook signing secret",
191
+ "description": "The whsec_ secret of the site's Stripe webhook endpoint. Vouchers are issued only through it. Stored encrypted."
192
+ },
193
+ "pairingMasterKey": {
194
+ "type": "secret",
195
+ "label": "Pairing master key",
196
+ "description": "Events pairing secrets are derived from this key. Generated when you pair Events; changing it un-pairs Events."
197
+ },
198
+ "licenceKey": {
199
+ "type": "secret",
200
+ "label": "Pro licence key",
201
+ "description": "Your Events Pro (or Suite) semics1. key. Not needed on local or preview hosts. Stored encrypted."
202
+ }
203
+ }
204
+ }
205
+ }
package/package.json CHANGED
@@ -1,11 +1,47 @@
1
1
  {
2
- "name": "@semics-tech/emdash-vouchers-pro",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Placeholder for @semics-tech/emdash-vouchers-pro, published once so npm trusted publishing can be configured. No code.",
5
- "license": "SEE LICENSE IN LICENSE",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/semics-uk/emdash-plugins-pro",
9
- "directory": "packages/vouchers-pro"
10
- }
11
- }
2
+ "name": "@semics-tech/emdash-vouchers-pro",
3
+ "version": "1.0.0-rc.12",
4
+ "type": "module",
5
+ "description": "Gift vouchers for EmDash: sold through Stripe, redeemed in Events bookings (proprietary)",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/semics-uk/emdash-plugins-pro",
10
+ "directory": "packages/vouchers-pro"
11
+ },
12
+ "main": "dist/index.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.mjs",
16
+ "types": "./dist/index.d.mts"
17
+ },
18
+ "./sandbox": "./dist/plugin.mjs"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "emdash-plugin.jsonc",
23
+ "LICENSE",
24
+ "!**/*.map"
25
+ ],
26
+ "peerDependencies": {
27
+ "emdash": ">=1.2.0 <2"
28
+ },
29
+ "devDependencies": {
30
+ "@emdash-cms/plugin-cli": "0.13.3",
31
+ "@emdash-cms/plugin-test": "0.2.8",
32
+ "@semics-tech/shared": "link:../../../emdash-plugins/packages/shared",
33
+ "emdash": "1.2.0",
34
+ "typescript": "5.9.3",
35
+ "vitest": "4.1.12",
36
+ "@semics-tech/pro-kit": "0.0.0"
37
+ },
38
+ "scripts": {
39
+ "generate": "node ../../scripts/generate.mjs vouchers-pro",
40
+ "validate": "pnpm run generate && node ../pro-kit/bin/validate-pro.ts",
41
+ "build": "pnpm run generate && emdash-plugin build",
42
+ "bundle": "pnpm run generate && emdash-plugin bundle",
43
+ "typecheck": "pnpm run generate && tsc --noEmit",
44
+ "test": "vitest run --reporter=dot",
45
+ "verify": "pnpm run validate && pnpm run typecheck && pnpm run test && pnpm run build"
46
+ }
47
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @semics-tech/emdash-vouchers-pro
2
-
3
- Placeholder; no code.