@semics-tech/emdash-vouchers-pro 0.0.0-stage → 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.
- package/LICENSE +106 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +264 -0
- package/dist/manifest.json +269 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.mjs +5 -0
- package/emdash-plugin.jsonc +205 -0
- package/package.json +44 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
semics Pro plugins for EmDash: commercial licence
|
|
2
|
+
Version 1.0
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 SEMICS LTD. All rights reserved.
|
|
5
|
+
|
|
6
|
+
1. Who this licence is between
|
|
7
|
+
|
|
8
|
+
This licence is between SEMICS LTD (company number 12871299), registered in England and Wales, registered
|
|
9
|
+
office Slowley Hall, Tamworth Road, Coventry, CV7 8EB ("we", "us"), and the person or organisation that
|
|
10
|
+
bought the licence ("you"). Contact: support@semics.co.uk.
|
|
11
|
+
|
|
12
|
+
It covers the semics Pro plugin packages for EmDash (@semics-tech/emdash-events-pro and
|
|
13
|
+
@semics-tech/emdash-esign-pro), with their documentation and updates ("the Software"). You buy a licence
|
|
14
|
+
through our online reseller Paddle.com, which is the Merchant of Record; our terms of sale at
|
|
15
|
+
https://plugins.semics.co.uk/terms also apply.
|
|
16
|
+
|
|
17
|
+
2. The licence key and the Update Term
|
|
18
|
+
|
|
19
|
+
When you buy or renew, you receive a licence key for your plan. The key names the production hostnames it
|
|
20
|
+
covers and the products it unlocks: one production hostname for Events Pro, E-sign Pro and Suite Pro, or
|
|
21
|
+
up to 10 production hostnames for Agency. Each production hostname needs its own place in a key.
|
|
22
|
+
|
|
23
|
+
The "Update Term" is one year from the date you pay. Each renewal adds another year.
|
|
24
|
+
|
|
25
|
+
3. What you may do
|
|
26
|
+
|
|
27
|
+
We grant you a non-exclusive, non-transferable licence to:
|
|
28
|
+
|
|
29
|
+
(a) install and run the Software on the production hostnames your key covers;
|
|
30
|
+
(b) install and run it without a key, in any number, on local and preview hosts used for development and
|
|
31
|
+
testing: localhost, 127.0.0.1, [::1], *.localhost, *.workers.dev and *.pages.dev;
|
|
32
|
+
(c) make the copies you need for those installs, for backups and for your build and deployment pipeline;
|
|
33
|
+
(d) modify the Software for your own use on the hostnames this licence covers, for example to fix a bug or
|
|
34
|
+
fit it to your site; and
|
|
35
|
+
(e) let your staff and contractors do these things for you, as long as they follow this licence.
|
|
36
|
+
|
|
37
|
+
An agency may install the Software for its clients on the hostnames its key covers. The licence stays with
|
|
38
|
+
the buyer unless we agree in writing to move it.
|
|
39
|
+
|
|
40
|
+
4. Updates, and the versions you keep
|
|
41
|
+
|
|
42
|
+
During the Update Term you may install every version of the Software we release, and you get email
|
|
43
|
+
support.
|
|
44
|
+
|
|
45
|
+
Your right to use each version released during the Update Term is perpetual: it does not end when the
|
|
46
|
+
Update Term ends, when you cancel, or when you stop renewing. Section 7 sets out the only exceptions: a
|
|
47
|
+
refund, and our ending this licence for a serious breach. A version released after your Update Term ends
|
|
48
|
+
needs a renewed key; the key you already have does not unlock it.
|
|
49
|
+
|
|
50
|
+
The key is checked offline on your site. We cannot switch it off remotely, and your site never needs to
|
|
51
|
+
contact us for the Software to keep working.
|
|
52
|
+
|
|
53
|
+
5. What you must not do
|
|
54
|
+
|
|
55
|
+
You must not:
|
|
56
|
+
|
|
57
|
+
(a) sell, rent, lend, sublicense, redistribute or publish the Software or a modified version of it, or
|
|
58
|
+
offer it to others as a product, plugin or hosted service of its own;
|
|
59
|
+
(b) share your licence key, or use it on hostnames it does not cover;
|
|
60
|
+
(c) remove, disable or get around the licence check, or help anyone else do so; or
|
|
61
|
+
(d) remove copyright or licence notices.
|
|
62
|
+
|
|
63
|
+
Nothing here stops you doing what the law allows you to do whatever a contract says, such as making a
|
|
64
|
+
back-up copy or decompiling for interoperability where the law permits it.
|
|
65
|
+
|
|
66
|
+
6. Your data and the free plugins
|
|
67
|
+
|
|
68
|
+
The Software stores its data in your own site. Your data stays yours: when your licence ends, or you go
|
|
69
|
+
back to the free plugins, nothing is deleted or locked, and you can export it.
|
|
70
|
+
|
|
71
|
+
The free plugins (Events and E-sign) are open source under the MIT licence. The parts of this package that
|
|
72
|
+
come from them remain available to you under MIT; this licence covers the Pro additions.
|
|
73
|
+
|
|
74
|
+
7. Refunds and ending this licence
|
|
75
|
+
|
|
76
|
+
You may have a full refund within 14 days of a purchase or renewal, for any reason (see
|
|
77
|
+
https://plugins.semics.co.uk/refunds). What a full refund does depends on the payment refunded:
|
|
78
|
+
|
|
79
|
+
(a) A refund of your first purchase ends this licence. You must stop using the Software and remove the
|
|
80
|
+
key from your site.
|
|
81
|
+
(b) A refund of a renewal ends only the refunded year. You keep the versions released during the earlier
|
|
82
|
+
years you paid for, and we will email you a key for them. You must stop using versions released only
|
|
83
|
+
during the refunded year and replace the key issued for it. We issue no further keys or updates and
|
|
84
|
+
give no further support for this licence.
|
|
85
|
+
|
|
86
|
+
A chargeback counts as a refund of the payment charged back. A partial refund, such as a tax correction,
|
|
87
|
+
ends nothing. If we refund you because the Software was faulty, you keep any other remedy the law gives
|
|
88
|
+
you.
|
|
89
|
+
|
|
90
|
+
If you seriously break section 5 and do not put it right within 30 days of our written notice, we may end
|
|
91
|
+
this licence. You must then stop using the Software and delete your copies.
|
|
92
|
+
|
|
93
|
+
When your Update Term ends without renewal, and after (b), sections 3 to 9 continue for the versions you
|
|
94
|
+
keep. After (a), or after we end this licence for a breach, sections 5, 6, 8 and 9 continue.
|
|
95
|
+
|
|
96
|
+
8. Our responsibility
|
|
97
|
+
|
|
98
|
+
Our responsibility for the Software, and the limits on it, are set out in our terms of sale at
|
|
99
|
+
https://plugins.semics.co.uk/terms. If you are a consumer, nothing in this licence affects your legal
|
|
100
|
+
rights.
|
|
101
|
+
|
|
102
|
+
9. Law
|
|
103
|
+
|
|
104
|
+
This licence is governed by the law of England and Wales, and the courts of England and Wales have
|
|
105
|
+
jurisdiction. If you are a consumer living elsewhere in the UK or the EU, you also keep the protection of
|
|
106
|
+
the law where you live and may bring a claim in your local courts.
|
package/dist/index.d.mts
ADDED
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
|
+
}
|
package/dist/plugin.mjs
ADDED
|
@@ -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,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semics-tech/emdash-vouchers-pro",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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
|
+
}
|
|
6
47
|
}
|
package/README.md
DELETED