@vermeulen.solutions/eventual-tickets 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -1,41 +1,47 @@
1
- # Changelog
2
-
3
- ## 0.1.0 (2026-11-14)
4
-
5
- ### Added
6
-
7
- - Ticket-type rows in the event panel: name, price, tickets per date, most per order and a sale window. A rename keeps the type's id and its sales; a type with sales cannot be removed. The one-per-line text format remains for scripts.
8
- - Card payment through Stripe Checkout, with seats held while the buyer pays and all prices calculated on the server. Free ticket orders are confirmed without calling Stripe. After paying, the return page shows the order link to the browser that paid, so the ticket does not depend on the email arriving.
9
- - Pay by invoice or bank transfer: the instructions are emailed, seats are confirmed at checkout, Mark paid issues the ticket, and Cancel releases the seats. Online payment can be turned off per event, for an invoice-only checkout with a matching button and note.
10
- - Buyer order page with the ticket code and its QR code, the order status, and self-service cancellation until the deadline counted from the event's own start.
11
- - Door check-in by typed code, camera QR, or the QR code's URL. It admits a ticket once and refuses unpaid, refunded, other-event and other-date tickets.
12
- - Refunds from the Orders page or in Stripe, with partial refunds recorded and a full refund releasing the seats. Automatic refunds run every 5 minutes for paid orders that got no seats or paid a different amount or currency.
13
- - Orders page: orders that need attention, invoices awaiting payment, filters by event, date and status, buyer lookup, personal-data erasure, and a single-use CSV export with local times. Each order shows the answers to the registration questions (who the places are for) and its times in the event's time zone.
14
- - Orders page: "Places sold per event" lists each event (and date) with the places sold per ticket type, the total and the places in checkout, with a button to that event's orders. Expired and refunded orders are hidden from the list until shown with a button; choosing a status shows them, and the CSV always includes every order.
15
- - Dashboard widget "Recent orders": the five newest paid or invoiced orders, and a link to Orders.
16
- - Registration questions on the order form, validated on the server before any seat is held.
17
- - A default time zone setting for events that have none (a collection without a `timezone` field, such as camps with only a start date-time): their times show in it instead of UTC. Empty keeps UTC. On the Tickets setup page.
18
- - The webhook checks Stripe's signature over the raw request bytes and never confirms a payment of the wrong amount or currency.
19
- - 21 languages: English, French, German, Dutch, Spanish, Italian, Portuguese (Portugal and Brazil), Polish, Czech, Swedish, Danish, Norwegian Bokmål, Finnish, Japanese, Simplified Chinese, Korean, Turkish, Ukrainian, Arabic and Hebrew, with right-to-left layout for Arabic and Hebrew, in all four site components, every buyer email and the admin (event panel, Orders page and setup page), which follows the EmDash UI language. Admin and email translations are language packs downloaded from `cdn.jsdelivr.net` and checked against hashes built into the release; English is built in.
20
- - Licence activation: paste the licence code from the purchase email on the Tickets page and click Activate; Tickets fetches a key for this site's hostname once from `licences.eventual.rsvp`. Refresh and Deactivate (to move the licence to another site) are on the same page. A hand-issued key can still be pasted.
21
- - Offline signed licence (`evt1…`, Ed25519): it gates new checkouts only. Development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) need no key, and existing orders, refunds, webhooks and check-in keep working.
22
-
23
- ### For site builders
24
-
25
- - One npm package, `@vermeulen.solutions/eventual-tickets`, with the site components under `./astro`: `TicketCheckout.astro`, `TicketCheckoutReturn.astro`, `TicketOrder.astro` and `TicketCheckin.astro`. Their card width follows `--et-card-max-width`, so a page can let the form fill its column; on dark sites the order page's Cancel button keeps a transparent background with a text-coloured border.
26
- - Three ways to install: npm sandboxed, npm native, and the EmDash registry once published. A registry install has an ID like `r_…`; the plugin page shows the exact path to pass as `apiBase`.
27
- - The setup page shows the webhook URL and the Stripe events to subscribe to. Stripe secret keys and the webhook secret are encrypted settings and need `EMDASH_ENCRYPTION_KEY`; free tickets and pay by invoice work without it.
28
- - The only outbound hosts are `api.stripe.com`, `challenges.cloudflare.com`, `cdn.jsdelivr.net` (language packs) and `licences.eventual.rsvp` (licence activation, only on an administrator's click). Every route, hook and cron task stays within 10 host calls.
29
- - The site components need `@vermeulen.solutions/eventual-locales`, installed with the package.
30
- - `trust-contract.json` records the trust contract; `npm test` fails when it changes.
31
- - `npm run release:check` checks that the package is ready to publish; `npm run release:date` sets the build date; `npm run licence:apply` builds `LICENSE` from the prepared text and the owner's facts.
32
-
33
- ### Limits
34
-
35
- - Card payments, abandoned checkouts, refunds and buyer cancellations are proven on Cloudflare in Stripe test mode, not yet in live mode. The runtime suite never calls Stripe.
36
- - Until a language pack has been downloaded (about five minutes after installing or updating), that language's emails and admin pages are in English.
37
- - Licence codes are emailed after a purchase on the website and activated from the Tickets page, which needs `licences.eventual.rsvp` to answer at that moment; checkouts never call it.
38
- - The plugin bundle is about 121.5 KB against a self-set budget of 120 KiB (122,880 B); the registry's limit is 128 KiB.
39
- - Admin sidebar labels come from the English manifest, so they stay English on a French admin; the pages themselves are translated.
40
- - The camera check-in path is not tested. Typed codes and QR URLs are.
41
- - CSV export and reconcile work in pages: about 700 orders per CSV and about 700 per reconcile pass. The README's "Large events" section says what to do past those.
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-11-14)
4
+
5
+ ### Changed
6
+
7
+ - Licences are yearly subscriptions (licence version 1.2). A renewal extends the same licence code; the site picks up the renewed key by itself: once a day, from a week before the end of the paid year until a month after it (or whenever its key no longer covers the installed version), Tickets asks `licences.eventual.rsvp` for this site's key, sending only the code and the hostname, and keeps the new key if it covers more. The check runs in the 05:00 UTC slot of the hourly retention task, so no new task is scheduled.
8
+
9
+ ## 0.1.0 (2026-11-14)
10
+
11
+ ### Added
12
+
13
+ - Ticket-type rows in the event panel: name, price, tickets per date, most per order and a sale window. A rename keeps the type's id and its sales; a type with sales cannot be removed. The one-per-line text format remains for scripts.
14
+ - Card payment through Stripe Checkout, with seats held while the buyer pays and all prices calculated on the server. Free ticket orders are confirmed without calling Stripe. After paying, the return page shows the order link to the browser that paid, so the ticket does not depend on the email arriving.
15
+ - Pay by invoice or bank transfer: the instructions are emailed, seats are confirmed at checkout, Mark paid issues the ticket, and Cancel releases the seats. Online payment can be turned off per event, for an invoice-only checkout with a matching button and note.
16
+ - Buyer order page with the ticket code and its QR code, the order status, and self-service cancellation until the deadline counted from the event's own start.
17
+ - Door check-in by typed code, camera QR, or the QR code's URL. It admits a ticket once and refuses unpaid, refunded, other-event and other-date tickets.
18
+ - Refunds from the Orders page or in Stripe, with partial refunds recorded and a full refund releasing the seats. Automatic refunds run every 5 minutes for paid orders that got no seats or paid a different amount or currency.
19
+ - Orders page: orders that need attention, invoices awaiting payment, filters by event, date and status, buyer lookup, personal-data erasure, and a single-use CSV export with local times. Each order shows the answers to the registration questions (who the places are for) and its times in the event's time zone.
20
+ - Orders page: "Places sold per event" lists each event (and date) with the places sold per ticket type, the total and the places in checkout, with a button to that event's orders. Expired and refunded orders are hidden from the list until shown with a button; choosing a status shows them, and the CSV always includes every order.
21
+ - Dashboard widget "Recent orders": the five newest paid or invoiced orders, and a link to Orders.
22
+ - Registration questions on the order form, validated on the server before any seat is held.
23
+ - A default time zone setting for events that have none (a collection without a `timezone` field, such as camps with only a start date-time): their times show in it instead of UTC. Empty keeps UTC. On the Tickets setup page.
24
+ - The webhook checks Stripe's signature over the raw request bytes and never confirms a payment of the wrong amount or currency.
25
+ - 21 languages: English, French, German, Dutch, Spanish, Italian, Portuguese (Portugal and Brazil), Polish, Czech, Swedish, Danish, Norwegian Bokmål, Finnish, Japanese, Simplified Chinese, Korean, Turkish, Ukrainian, Arabic and Hebrew, with right-to-left layout for Arabic and Hebrew, in all four site components, every buyer email and the admin (event panel, Orders page and setup page), which follows the EmDash UI language. Admin and email translations are language packs downloaded from `cdn.jsdelivr.net` and checked against hashes built into the release; English is built in.
26
+ - Licence activation: paste the licence code from the purchase email on the Tickets page and click Activate; Tickets fetches a key for this site's hostname once from `licences.eventual.rsvp`. Refresh and Deactivate (to move the licence to another site) are on the same page. A hand-issued key can still be pasted.
27
+ - Offline signed licence (`evt1…`, Ed25519): it gates new checkouts only. Development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) need no key, and existing orders, refunds, webhooks and check-in keep working.
28
+
29
+ ### For site builders
30
+
31
+ - One npm package, `@vermeulen.solutions/eventual-tickets`, with the site components under `./astro`: `TicketCheckout.astro`, `TicketCheckoutReturn.astro`, `TicketOrder.astro` and `TicketCheckin.astro`. Their card width follows `--et-card-max-width`, so a page can let the form fill its column; on dark sites the order page's Cancel button keeps a transparent background with a text-coloured border.
32
+ - Three ways to install: npm sandboxed, npm native, and the EmDash registry once published. A registry install has an ID like `r_…`; the plugin page shows the exact path to pass as `apiBase`.
33
+ - The setup page shows the webhook URL and the Stripe events to subscribe to. Stripe secret keys and the webhook secret are encrypted settings and need `EMDASH_ENCRYPTION_KEY`; free tickets and pay by invoice work without it.
34
+ - The only outbound hosts are `api.stripe.com`, `challenges.cloudflare.com`, `cdn.jsdelivr.net` (language packs) and `licences.eventual.rsvp` (licence activation on an administrator's click; from 0.1.1 also the daily renewal check). Every route, hook and cron task stays within 10 host calls.
35
+ - The site components need `@vermeulen.solutions/eventual-locales`, installed with the package.
36
+ - `trust-contract.json` records the trust contract; `npm test` fails when it changes.
37
+ - `npm run release:check` checks that the package is ready to publish; `npm run release:date` sets the build date; `npm run licence:apply` builds `LICENSE` from the prepared text and the owner's facts.
38
+
39
+ ### Limits
40
+
41
+ - Card payments, abandoned checkouts, refunds and buyer cancellations are proven on Cloudflare in Stripe test mode, not yet in live mode. The runtime suite never calls Stripe.
42
+ - Until a language pack has been downloaded (about five minutes after installing or updating), that language's emails and admin pages are in English.
43
+ - Licence codes are emailed after a purchase on the website and activated from the Tickets page, which needs `licences.eventual.rsvp` to answer at that moment; checkouts never call it.
44
+ - The plugin bundle is about 121.5 KB against a self-set budget of 120 KiB (122,880 B); the registry's limit is 128 KiB.
45
+ - Admin sidebar labels come from the English manifest, so they stay English on a French admin; the pages themselves are translated.
46
+ - The camera check-in path is not tested. Typed codes and QR URLs are.
47
+ - CSV export and reconcile work in pages: about 700 orders per CSV and about 700 per reconcile pass. The README's "Large events" section says what to do past those.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  # Eventual RSVP – Tickets Licence
2
2
 
3
- Version 1.1, 2026-11-13
3
+ Version 1.2, 2026-11-14
4
4
 
5
5
  Copyright © 2026 Jaap Vermeulen, trading as Vermeulen Solutions. All rights reserved.
6
6
 
@@ -63,16 +63,18 @@ Copyright © 2026 Jaap Vermeulen, trading as Vermeulen Solutions. All rights res
63
63
  3. Licences are sold through Paddle, which acts as reseller and merchant of record: Paddle takes the payment, adds the taxes that apply in the buyer's country, and issues the invoice. Paddle's buyer terms apply to the payment. This licence governs the software licence. If Paddle's terms conflict with this licence on the software licence, this licence prevails; if they conflict on payment, Paddle's terms prevail.
64
64
  4. Where the checkout shows an amount in another currency, that amount is a conversion of the dollar price at the time of payment.
65
65
  5. The licensor takes no share of ticket sales. Fees charged by Stripe or any other payment provider for the licensee's ticket sales are between the licensee and that provider.
66
- 6. A licence does not renew by itself. The licensee renews by buying a further year. The price at renewal is the price published at that time; an active term is not affected by a later price change.
67
- 7. If a payment is reversed by a chargeback or similar payment dispute, the licence ends unless the licensee pays the amount again. Access may be suspended while the dispute is investigated.
66
+ 6. **A licence is a yearly subscription.** At the end of each term it renews for a further year, and Paddle charges the licence price to the payment method used for the purchase, until the licensee cancels. The licensor emails a reminder about 30 days before each renewal. A renewal is charged at the price the licensee subscribed at, unless the licensor announces a different price at least 30 days before that renewal; the licensee may then cancel before it.
67
+ 7. **Cancelling.** The licensee may cancel at any time, with the link in Paddle's receipt email or by writing to the support address. Cancelling stops future renewals; the current term runs to its end. A cancelled term is not refunded except under §12.
68
+ 8. If a renewal payment fails, Paddle tries it again for a limited time. The term is extended only once the payment succeeds; if it does not succeed, the licence ends at the end of the term already paid, as if cancelled.
69
+ 9. If a payment is reversed by a chargeback or similar payment dispute, the licence ends unless the licensee pays the amount again. Access may be suspended while the dispute is investigated.
68
70
 
69
71
  ## 6. Term, licence keys and what happens when a licence ends
70
72
 
71
- 1. A licence runs for one year from the day of purchase. It does not renew by itself; the licensee renews by buying a further year.
73
+ 1. A licence runs for one year from the day of purchase (the **term**). Each renewal under §5.6 extends the term by a further year.
72
74
  2. For each licence the licensor issues a **licence code**, sent by email after the purchase. The licensee activates each licensed site with the code, from that site's Tickets administration page. The software then receives a **licence key** for the site's hostname and its `www` variant. A key states the hostnames it covers and the date until which the licensee receives new versions. The licensee may not have more sites active at once than the licence covers.
73
75
  3. The licensee may deactivate a site, from that site or from the licensor's licence page, to activate another site in its place. A key keeps working on its site after deactivation, so a licence may be moved at most three times per site it covers in any twelve months; an unlimited licence may be moved without limit. If the code does not arrive or is lost, the licensee can have it sent again to the purchase email address.
74
- 4. The software checks the key on the site itself. It contacts the licensor's licence service only when an administrator activates, refreshes or deactivates the licence, and then sends only the licence code and the site's hostname. It never sends attendee, buyer or order data. The licensee is responsible for keeping the code and keys confidential.
75
- 5. **A licensed site keeps working after the licence ends.** The licensee keeps the right to use, on the hostnames named on the key and without limit in time, every version of the software released on or before the last day of the term. Only versions released after that day need a renewed licence.
76
+ 4. The software checks the key on the site itself. It contacts the licensor's licence service only when an administrator activates, refreshes or deactivates the licence, and once a day from a week before the end of the term until a month after it, to pick up the key for a renewed term. Each time it sends only the licence code and the site's hostname. It never sends attendee, buyer or order data. The licensee is responsible for keeping the code and keys confidential.
77
+ 5. **A licensed site keeps working after the licence ends.** The licensee keeps the right to use, on the hostnames named on the key and without limit in time, every version of the software released on or before the last day of the term. Only versions released after that day need a renewed or new licence.
76
78
  6. Without a valid key for its hostname, a site outside §3 cannot start new ticket orders or checkouts, including free tickets. Orders that already exist keep working in every case: buyers can open them, tickets can be checked at the door, and refunds and cancellations can be handled.
77
79
  7. If the licensor ceases to operate, existing keys and the versions released during the term continue to work on the hostnames they name. No new activations or keys can be issued after that point.
78
80
 
@@ -123,8 +125,8 @@ If the licensee materially breaches §8 or §9 and does not put it right within
123
125
 
124
126
  ## 12. Refunds and chargebacks
125
127
 
126
- 1. The licensee may ask for a full refund within 30 days of a purchase, without giving a reason, by writing to the support address shown on the Support page and including the Paddle receipt or order reference.
127
- 2. The refund is paid back through Paddle to the payment method used. The licence ends when the refund is paid, not when it is requested.
128
+ 1. The licensee may ask for a full refund of a payment within 30 days of that payment, including a renewal payment, without giving a reason, by writing to the support address shown on the Support page and including the Paddle receipt or order reference.
129
+ 2. The refund is paid back through Paddle to the payment method used, and the subscription is cancelled. For a first purchase the licence ends when the refund is paid, not when it is requested. For a renewal payment the term returns to the end it had before that renewal.
128
130
  3. After the refund, the licensee must stop new ticket orders and checkouts. Existing orders, tickets, door check-in, cancellations and refunds to the licensee's own buyers continue to work.
129
131
  4. Nothing in this section limits a statutory withdrawal or refund right. A Consumer in the EU/EEA or the UK has the statutory 14-day withdrawal right provided by Paddle's buyer terms; the 30-day policy in this section is additional. A Swiss Consumer has the rights that Swiss law provides; Swiss law does not create a general online withdrawal right.
130
132
  5. If a payment is reversed by a chargeback or similar dispute, the licence ends unless the licensee pays the amount again. Access may be suspended while the dispute is investigated.
package/README.md CHANGED
@@ -1,203 +1,203 @@
1
- # Eventual RSVP – Tickets
2
-
3
- Paid tickets for [Eventual RSVP](https://github.com/Vermeulen-Solutions/eventual-emdash-sandbox) events in EmDash: priced tiers, Stripe Checkout, invoices and signed ticket codes, as a sandboxed plugin.
4
-
5
- **Version 0.1.0.** Tiers, Stripe Checkout, invoices, orders, refunds, the buyer's order page, door check-in, and 21 languages work and are tested on EmDash's runtime test host and in the local browser test site; Stripe test mode has been exercised locally, but card payments have not yet been run on Cloudflare. See [docs/SPECIFICATION.md](docs/SPECIFICATION.md), whose §1 is the status table.
6
-
7
- ## How it fits the suite
8
-
9
- - Sells seats only for events whose **Seats managed by** field (`inventory_mode`, from the capacity field pack) is `tickets`. RSVP events belong to Eventual RSVP – Registrations. Each event has exactly one owner, so the two plugins can never sell the same seat.
10
- - Keeps its own seat ledger, built from the shared [`eventual-capacity`](https://github.com/Vermeulen-Solutions/eventual-capacity) library: the same zero-overselling code as Attendance, with separate data.
11
- - Talks to `api.stripe.com` (payments), `challenges.cloudflare.com` (Turnstile, only when you turn it on), `cdn.jsdelivr.net` (language packs) and `licences.eventual.rsvp` (only when you click Activate, Refresh or Deactivate on the licence). Ticket sales never wait on an Eventual RSVP service: the licence key is checked offline.
12
-
13
- ## Installation
14
-
15
- Add it to the EmDash integration in `astro.config.mjs`, sandboxed:
16
-
17
- ```js
18
- emdash({
19
- // …database, storage, your other plugins
20
- sandboxed: [eventualTickets],
21
- sandboxRunner: "@emdash-cms/cloudflare/sandbox", // on Cloudflare
22
- })
23
- ```
24
-
25
- On Cloudflare this needs the **Workers Paid** plan, a `"worker_loaders": [{ "binding": "LOADER" }]` binding in `wrangler.jsonc`, and `export { PluginBridge } from "@emdash-cms/cloudflare/worker";` in your Worker entry. The [Attendance README](https://github.com/Vermeulen-Solutions/eventual-attendance#running-sandboxed-on-cloudflare) explains each step. Ticket emails need an email provider plugin.
26
-
27
- ### Three ways to install
28
-
29
- **1. From the EmDash registry** (admin → Plugins → Registry).
30
-
31
- - **Run:** install Eventual RSVP – Tickets from the registry, and add the npm package for the components: `npm install @vermeulen.solutions/eventual-tickets`. The npm package is not published yet, so this route becomes available when it is. *(Not yet tested by us.)*
32
- - **Config:** the registry install is sandboxed; nothing else goes in `astro.config.mjs`.
33
- - **Pages:** the order page (default `/tickets/order`) with `TicketOrder`, and the return page (default `/tickets/checkout`) with `TicketCheckoutReturn`. Put `TicketCheckout` on each event page; the check-in page is optional.
34
- - **Check it worked:** open **Tickets** in the admin and copy the `apiBase` it shows into the components. Buy one free ticket on a test event and open the order page.
35
-
36
- **2. From npm, sandboxed.**
37
-
38
- - **Run:** `npm install @vermeulen.solutions/eventual-tickets`, then add it to `astro.config.mjs` as in the example above.
39
- - **Config:** `sandboxed: [eventualTickets]` plus a `sandboxRunner` (`@emdash-cms/cloudflare/sandbox` on Cloudflare, `@emdash-cms/sandbox-workerd/sandbox` on Node), as in the example above.
40
- - **Pages:** the order page with `TicketOrder`, the return page with `TicketCheckoutReturn`, and `TicketCheckout` on each event page; the check-in page is optional.
41
- - **Check it worked:** open **Tickets** in the admin; it shows the webhook URL and the Stripe events when card payment is configured. Buy one free ticket on a published test event.
42
-
43
- **3. From npm, native.**
44
-
45
- - **Run:** `npm install @vermeulen.solutions/eventual-tickets`.
46
- - **Config:** `plugins: [eventualTickets]`. This runs in the site's own process, without the sandbox's limits or isolation; only on hosts you trust.
47
- - **Pages:** the order page, the return page and `TicketCheckout` on event pages; the check-in page is optional.
48
- - **Check it worked:** as for the sandboxed npm install. Tested on Node in the local test site (`EVENTUAL_PLUGIN_MODE=native`, `npm run test:integration:suite` and `npm run test:e2e`).
49
-
50
- ### Permissions and why
51
-
52
- | Permission | Why |
53
- | :--- | :--- |
54
- | `content:read` | Read the event (title, dates, published or not) an order is for |
55
- | `email:send` | Confirmations, instructions, refund notices, ticket links |
56
- | `network:request` to `api.stripe.com` | Create Checkout sessions, check session status, and issue refunds |
57
- | `network:request` to `challenges.cloudflare.com` | Check Cloudflare Turnstile tokens, only when you turn Turnstile on |
58
- | `network:request` to `cdn.jsdelivr.net` | Download the admin and email translations (language packs from `@vermeulen.solutions/eventual-locales`); each file is checked against a hash built into the plugin |
59
- | `network:request` to `licences.eventual.rsvp` | Turn your licence code into a key for this site, and free the site again. Sends only the code and the site's hostname, and only when an administrator clicks Activate, Refresh or Deactivate |
60
-
61
- The licence check itself uses Web Crypto inside the worker and makes no network requests.
62
-
63
- #### Public routes
64
-
65
- The plugin declares the following public routes (listed in `trust-contract.json`):
66
-
67
- | Route | Method | Purpose |
68
- | :--- | :--- | :--- |
69
- | `availability` | GET | Check ticket availability and pricing tiers for an event occurrence |
70
- | `orders/download` | GET | Single-use download link for orders CSV generated by the admin panel |
71
- | `checkout` | POST | Create an order and initiate Stripe Checkout (or confirm free order) |
72
- | `checkout/release` | POST | Release held seats when a buyer abandons checkout |
73
- | `checkout/status` | POST | Check Stripe payment status after returning from Checkout |
74
- | `order` | POST | Look up order details and ticket codes using a signed HMAC token |
75
- | `stripe/webhook` | POST | Webhook receiver for Stripe checkout completion and expiration events |
76
-
77
- **Installed from the registry?** The plugin then has an ID like `r_…` instead of its name. The plugin's admin page shows the exact paths; pass them to the site components (`apiEndpoint` / `apiBase`).
78
-
79
- **Spam protection (optional).** Every form has a hidden honeypot and a timing check. When Turnstile is off, Tickets refuses more than **10 orders from one IP address in 10 minutes** (keeping only a 32-hex keyed hash `ipHash`, never the raw IP). Off Cloudflare, EmDash knows the visitor's IP only when its trusted proxy headers are configured; without an IP there is no limit. When Turnstile is on, Cloudflare's challenge handles rate abuse and the IP check is skipped. To turn Turnstile on, paste a Cloudflare Turnstile site key and secret key on the **Tickets** admin page; the checkout form then shows Cloudflare's check. No encryption key is needed for it. If your site sends a Content Security Policy, allow `https://challenges.cloudflare.com` in `script-src` and `frame-src`. Details: the [Attendance README](https://github.com/Vermeulen-Solutions/eventual-attendance#spam-protection-optional-cloudflare-turnstile).
80
-
81
- ## Setup
82
-
83
- 1. **Encryption key (card payments only).** Free tickets and pay-by-invoice need no setup. To take card payments, Tickets keeps your Stripe keys as encrypted settings, so the site needs `EMDASH_ENCRYPTION_KEY` (`npx emdash secrets generate`; on Cloudflare `wrangler secret put EMDASH_ENCRYPTION_KEY`). The Tickets page tells you when it's missing.
84
- 2. Open **Tickets** in the admin. It shows the webhook URL and the Stripe events to subscribe to.
85
- 3. In the settings form under **Events**, paste your Stripe secret key (`sk_test_…` while testing) and the webhook signing secret (`whsec_…`).
86
- 4. Add the components to your site:
87
- - on the event page: `<TicketCheckout eventId={id} occurrenceStart={date} />` from `@vermeulen.solutions/eventual-tickets/astro/TicketCheckout.astro`
88
- - on the return page (default path `/tickets/checkout`, set on the Tickets page): `<TicketCheckoutReturn backHref="/events" />`
89
- - on the order page the ticket email links to (default `/tickets/order`): `<TicketOrder />`
90
- - on the door staff's page (`/tickets/check`, opened while signed in): `<TicketCheckin eventId={id} occurrenceStart={date} />`
91
-
92
- Props: `TicketCheckout` takes `eventId`, `occurrenceStart`, `apiBase` (default `/_emdash/api/plugins/eventual-tickets`; use the path the admin page shows on a registry install), `title` and `class`. `TicketCheckoutReturn` takes `apiBase`, `backHref` (default `/`) and `class`. `TicketOrder` takes `apiBase`, `checkinPath` (default `/tickets/check`; the QR opens it) and `class`. `TicketCheckin` takes `apiBase`, optional `eventId` / `occurrenceStart` (to refuse other events' tickets) and `class`.
93
-
94
- ### Event dates
95
-
96
- Tickets reads the event's own date, not only the date a form passes. It uses Core's `start`/`end`/`all_day`/`timezone`; for collections that keep `start_date`/`end_date` instead (camps, school series) it falls back to those, and a bare date there is an all-day event. That date is stored on the order (`startsAt`, `allDay`) and used in ticket and invoice emails, the Orders page and CSV, retention (counting from the event, not the order) and the delete guard. The seat ledger key is unchanged: `occurrenceStart` stays whatever the form sends, so existing ledgers keep working.
97
-
98
- ### Languages
99
-
100
- Tickets speaks English (built in), French, German, Dutch, Spanish, Italian, Portuguese (Portugal and Brazil), Polish, Czech, Swedish, Danish, Norwegian Bokmål, Finnish, Japanese, Simplified Chinese, Korean, Turkish, Ukrainian, Arabic and Hebrew. Arabic and Hebrew are laid out right to left.
101
-
102
- `TicketCheckout`, `TicketCheckoutReturn`, `TicketOrder` and `TicketCheckin` take a `locale` such as `"fr"`, `"de"` or `"pt-BR"` (default `"en"`); an unknown language falls back to the closest one, then English. The checkout sends its language with the order, and every buyer email for that order (ticket, invoice instructions, automatic-refund notice) is written in it; the order page switches to the order's language when it differs from the page's own. Prices and dates are formatted for the language (Swiss francs in the Swiss style). The components' texts come from the `@vermeulen.solutions/eventual-locales` npm package, installed with Tickets; each language is its own chunk, so a page loads only the one it shows.
103
-
104
- The admin pages, the editor panel and the Orders page follow the admin UI's language. The admin and email texts are not in the plugin bundle: English is built in, and the other languages are language packs the plugin downloads from `cdn.jsdelivr.net` (the public npm CDN) and keeps in its storage. A background task fetches them, the site's own language first, within about five minutes of installing or updating; until a language has arrived, its emails and admin pages are in English. Each pack is checked against a hash built into this release, so a release always uses exactly the translations it was tested with. The check-in verdicts an organiser sees and the organiser-facing emails are English. The plugin's sidebar labels come from the English manifest and stay English.
105
-
106
-
107
- ### Theming
108
-
109
- The components' CSS is global and prefixed (`.eventual-tickets-widget`, `.et-*`, `.etr-*`, `.eto-*`, `.etc-*`). Theme them by setting the `--et-*` variables on a wrapper around the component: colours `--et-primary`, `--et-primary-hover`, `--et-on-primary`, `--et-bg`, `--et-field-bg` (inputs and dropdowns; default white), `--et-border`, `--et-text`, `--et-muted`, `--et-success(-bg)`, `--et-warning(-bg)`, `--et-error(-bg)`, `--et-info-bg`; shapes `--et-radius` (cards), `--et-card-max-width` (default 480px; `none` fills the column), `--et-control-radius` (fields and messages), `--et-button-radius`; focus `--et-focus`, `--et-focus-ring`. Set a field's text and `--et-field-bg` together for a dark theme: the component paints both, so a light text colour alone would sit on a white field. Site CSS can also override a class directly (for example `.et-submit { text-transform: uppercase; }`), but variables survive a redesign and are the supported way.
110
-
111
- ### After booking
112
-
113
- When a free or invoice order is confirmed, the checkout page shows the buyer their **order reference** (`ord_…`) and, when the server could build it within the call budget, a link to their **order page** ("View your tickets" / "View or cancel your order"). The link is the same one the email carries, with the token in the URL fragment; it appears only in that browser's own answer, never in a GET request or query string. If it can't be built, the reference still is, and the page's texts only promise what it shows. Card buyers get the reference on the return page after paying; their order link comes by email.
114
-
115
- ### Orders and automatic refunds
116
-
117
- - View, filter and export orders (including `Local time` in the event's time zone) on the **Orders** admin page.
118
- - Paid card orders that arrived after seats sold out (`PAID_NO_SEATS`) or paid a mismatched amount/currency (`AMOUNT_MISMATCH`) get no ticket. With **Refund paid orders that got no seats, or paid the wrong amount, automatically** turned on (default `true` on the **Tickets** admin page), the `tickets-auto-refund` background task runs every 5 minutes, requests a full Stripe refund once (using the order's idempotency key), emails the buyer, and waits for Stripe's `charge.refunded` webhook; the Orders banner marks those orders as **refund pending**.
119
-
120
- ### Invoices
121
-
122
- Turn on **Offer payment by invoice or bank transfer** in the event's Tickets panel and write the payment instructions. Checkout confirms the seats at once and emails the bank details, the order reference and a link to the order page where the buyer can cancel. The email always comes first: on a budget-tight cold start it may arrive without the link rather than not at all. The organiser marks the invoice paid on the **Orders** page; that issues the ticket.
123
-
124
- ### Activating your licence
125
-
126
- - **Activate:** buying a licence emails you a licence code (`EVT-XXXX-XXXX-XXXX-XXXX`). On the **Tickets** admin page, paste it into **Licence** and click **Activate**. Tickets sends the code and this site's hostname to `licences.eventual.rsvp` once, and stores the key it gets back (for the hostname and its `www` twin) in plugin KV; no `EMDASH_ENCRYPTION_KEY` is needed. From then on the key is checked offline.
127
- - **More sites:** paste the same code on each site, up to the number of sites the licence covers (one, ten, or any number).
128
- - **Moving a site:** click **Deactivate this site** on the old site, then activate the new one. Moves are limited to 3 per site a year, because old keys keep working offline. A copied site with the code saved shows **Activate this site**.
129
- - **Refresh key:** asks for a fresh key for this site without using another place on the licence.
130
- - **Your sites:** `https://licences.eventual.rsvp/manage` lists every site that uses your code, with a Deactivate button for each, so a site you can no longer reach can be freed too.
131
- - **Lost code:** `https://licences.eventual.rsvp/resend` emails your codes to the address you bought with.
132
- - **A key instead of a code:** a key issued by hand (`evt1.…`) can be pasted into the same field.
133
- - **What the page shows when the key is valid:** a banner "Licensed for <hostname> (updates until <date>)." and the line "Licence: active for <hostname> (updates until <date>)."
134
- - **`LICENCE_REQUIRED`:** this hostname has no key. Paste your licence code and click Activate. Visitors see "Ticket sales are not available on this site right now."; existing orders keep working.
135
- - **`LICENCE_EXPIRED`:** the key covers versions released until a date, and this build is newer. Renew, or reinstall a version released during the paid year. The page says which date.
136
- - **Invalid for this hostname:** the key is valid but was issued for another hostname, for example after the site moved. Click **Activate this site**. The page says "Invalid licence key for <hostname>".
137
- - **Which hosts are free:** development and preview hosts (`localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, `*.workers.dev`, `*.pages.dev`) need no key and can always sell tickets.
138
- - **Nothing already sold breaks:** existing orders, Stripe webhooks, refunds, buyer order pages, QR check-in, retention and the Orders page are never blocked.
139
-
140
- ### Deleting events
141
-
142
- An event with upcoming orders (paid, pending or awaiting an invoice payment) can't be moved to the trash. Cancel them first, or unpublish the event to stop new bookings.
143
-
144
- ### Personal data
145
-
146
- Buyer names, email addresses and answers are personal data. Under privacy frameworks (such as Swiss FADP and EU GDPR), organisers are expected to retain personal details only as long as necessary and erase them on request.
147
-
148
- - **Automated retention:** The `tickets-retention` task runs hourly (at minute 23) and erases buyer names, email addresses and registration answers for orders whose event passed more than `retentionDays` ago (configurable on the Tickets admin page, default 365 days, 30–3650). Bookings for an event without a date are kept while they're live; the period starts again each time the sweep finds one. Financial and bookkeeping fields (amounts, currency, tier lines, order status, Stripe IDs) are preserved for statutory accounting obligations.
149
- - **Erasure on request:** On the **Orders** admin page, the **Personal data** section allows looking up orders by buyer email address. One click erases buyer names, email addresses and answers across past and cancelled orders. Active orders (pending, pending_invoice, or paid for upcoming/undated occurrences) are protected from erasure until cancelled or refunded. Erased orders stay in the list and CSV as "· erased" with `—` for the buyer so the money totals add up; a Refund action stays where a refund may still be owed.
150
- - **Stripe data:** Note that Stripe keeps its own independent record of customer and transaction data according to its own privacy policy and financial regulations; erasing data in EmDash erases local CMS records only.
151
- - **Starting point, not legal advice:** Retention rules depend on applicable legislation, tax requirements, and your organisation's policies.
152
-
153
- ## Large events
154
-
155
- These are the limits of one background task or download, given the sandbox's 10 host calls. Past a limit the plugin skips the work or truncates the file rather than failing.
156
-
157
- | What | Limit | What happens past it | What to do |
158
- | :--- | :--- | :--- | :--- |
159
- | Reconcile one date | ≈ 700 orders | The date is skipped (`TOO_LARGE`) and tried again on a later tick; its seat count is not corrected automatically | Split the event by date, so each date has its own pool |
160
- | Orders CSV export | ≈ 700 orders | The file is truncated with a note at the top | Filter by event and date and export each date separately |
161
- | Automatic refunds | 2 per 5-minute tick | Remaining refunds are requested on the following ticks | None needed |
162
- | Hold sweep | 2 overdue holds per minute | Remaining holds are checked on the following minutes | None needed |
163
-
164
- ## Development
165
-
166
- ```bash
167
- npm install # if npm 10.5 crashes ("edgesOut"), run: npm install --legacy-peer-deps && npm install
168
- npm run validate
169
- npm run typecheck
170
- npm test # EmDash runtime test host; Stripe is never called
171
- npm run build
172
- npm run release:check # one line per release gate; never publishes
173
- ```
174
-
175
- `npm run release:date` sets `BUILD_DATE` (today UTC, or `--date YYYY-MM-DD`) before a release build. It changes only that line, never the licence public key.
176
-
177
- `npm run licence:apply` builds `LICENSE` from `docs/LICENSE-DRAFT.md` and `licence-facts.json` (the owner fills in the licensor name, postal address and publication date). It refuses when a fact is missing or malformed, or when a placeholder is left; `-- --check` only reports.
178
-
179
- `trust-contract.json` records capabilities, hosts, hooks, public routes and storage indexes; `npm test` fails when the build differs. Change it only on purpose.
180
-
181
- `npm run release:check` runs the tests, type check, manifest check and build, then checks the manifest version, the changelog heading, what `npm pack` would contain, that every `astro/` file is self-contained, that the package is not `private`, and (Tickets) that the licence public key, build date and `LICENSE` are ready. It stops at the first failure and prints nothing but one line per step. It never publishes, tags, commits or uploads.
182
-
183
- The shared library is a dev dependency pinned to a commit on GitHub (private repo; needs read access).
184
-
185
- ## Troubleshooting
186
-
187
- - **Card payments are off:** paste a Stripe test/live secret key and the webhook signing secret on the **Tickets** admin page (the setup checklist names the missing piece). Free tickets and pay-by-invoice work without any Stripe key.
188
- - **"No valid licence key for …":** production hosts need a key whose hostnames include the site; development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) are exempt. An expired key blocks new sales only — existing orders, refunds, the webhook and check-in keep working.
189
- - **A buyer gets no email:** EmDash needs an email provider. Without one the confirmation says so and shows the order reference and the order link; the buyer can still get their ticket from that page.
190
- - **"Route not found" on a registry install:** use the `apiBase` shown on the Tickets page (the registry ID, not `eventual-tickets`).
191
- - **The webhook never arrives:** the endpoint URL must be the one shown on the Tickets page, subscribed to the events listed there, and the signing secret pasted into the settings. Stripe retries for three days.
192
- - **A public route answers `TRY_AGAIN`:** an unexpected host/database error was caught and logged; the raw message is never shown.
193
- - **A refund seems missing:** refunds made in Stripe appear after Stripe's `charge.refunded` webhook; the Orders page shows "part refunded" for partial amounts. Automatic refunds run every five minutes when the toggle is on.
194
-
195
- ## Licence
196
-
197
- Eventual RSVP – Tickets is commercial software. See [LICENSE](LICENSE) for the terms. Third-party components and their licences are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
198
-
199
- - Development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) need no key.
200
- - Every other site needs a licence: USD 30 per site per year, USD 100 per year for up to ten sites, USD 200 per year for any number of sites.
201
- - A licensed site keeps every version released during the paid year, for ever.
202
-
203
- Licences are bought on the suite's website.
1
+ # Eventual RSVP – Tickets
2
+
3
+ Paid tickets for [Eventual RSVP](https://github.com/Vermeulen-Solutions/eventual-emdash-sandbox) events in EmDash: priced tiers, Stripe Checkout, invoices and signed ticket codes, as a sandboxed plugin.
4
+
5
+ **Version 0.1.0.** Tiers, Stripe Checkout, invoices, orders, refunds, the buyer's order page, door check-in, and 21 languages work and are tested on EmDash's runtime test host and in the local browser test site; Stripe test mode has been exercised locally, but card payments have not yet been run on Cloudflare. See [docs/SPECIFICATION.md](docs/SPECIFICATION.md), whose §1 is the status table.
6
+
7
+ ## How it fits the suite
8
+
9
+ - Sells seats only for events whose **Seats managed by** field (`inventory_mode`, from the capacity field pack) is `tickets`. RSVP events belong to Eventual RSVP – Registrations. Each event has exactly one owner, so the two plugins can never sell the same seat.
10
+ - Keeps its own seat ledger, built from the shared [`eventual-capacity`](https://github.com/Vermeulen-Solutions/eventual-capacity) library: the same zero-overselling code as Attendance, with separate data.
11
+ - Talks to `api.stripe.com` (payments), `challenges.cloudflare.com` (Turnstile, only when you turn it on), `cdn.jsdelivr.net` (language packs) and `licences.eventual.rsvp` (only when you click Activate, Refresh or Deactivate on the licence). Ticket sales never wait on an Eventual RSVP service: the licence key is checked offline.
12
+
13
+ ## Installation
14
+
15
+ Add it to the EmDash integration in `astro.config.mjs`, sandboxed:
16
+
17
+ ```js
18
+ emdash({
19
+ // …database, storage, your other plugins
20
+ sandboxed: [eventualTickets],
21
+ sandboxRunner: "@emdash-cms/cloudflare/sandbox", // on Cloudflare
22
+ })
23
+ ```
24
+
25
+ On Cloudflare this needs the **Workers Paid** plan, a `"worker_loaders": [{ "binding": "LOADER" }]` binding in `wrangler.jsonc`, and `export { PluginBridge } from "@emdash-cms/cloudflare/worker";` in your Worker entry. The [Attendance README](https://github.com/Vermeulen-Solutions/eventual-attendance#running-sandboxed-on-cloudflare) explains each step. Ticket emails need an email provider plugin.
26
+
27
+ ### Three ways to install
28
+
29
+ **1. From the EmDash registry** (admin → Plugins → Registry).
30
+
31
+ - **Run:** install Eventual RSVP – Tickets from the registry, and add the npm package for the components: `npm install @vermeulen.solutions/eventual-tickets`. The npm package is not published yet, so this route becomes available when it is. *(Not yet tested by us.)*
32
+ - **Config:** the registry install is sandboxed; nothing else goes in `astro.config.mjs`.
33
+ - **Pages:** the order page (default `/tickets/order`) with `TicketOrder`, and the return page (default `/tickets/checkout`) with `TicketCheckoutReturn`. Put `TicketCheckout` on each event page; the check-in page is optional.
34
+ - **Check it worked:** open **Tickets** in the admin and copy the `apiBase` it shows into the components. Buy one free ticket on a test event and open the order page.
35
+
36
+ **2. From npm, sandboxed.**
37
+
38
+ - **Run:** `npm install @vermeulen.solutions/eventual-tickets`, then add it to `astro.config.mjs` as in the example above.
39
+ - **Config:** `sandboxed: [eventualTickets]` plus a `sandboxRunner` (`@emdash-cms/cloudflare/sandbox` on Cloudflare, `@emdash-cms/sandbox-workerd/sandbox` on Node), as in the example above.
40
+ - **Pages:** the order page with `TicketOrder`, the return page with `TicketCheckoutReturn`, and `TicketCheckout` on each event page; the check-in page is optional.
41
+ - **Check it worked:** open **Tickets** in the admin; it shows the webhook URL and the Stripe events when card payment is configured. Buy one free ticket on a published test event.
42
+
43
+ **3. From npm, native.**
44
+
45
+ - **Run:** `npm install @vermeulen.solutions/eventual-tickets`.
46
+ - **Config:** `plugins: [eventualTickets]`. This runs in the site's own process, without the sandbox's limits or isolation; only on hosts you trust.
47
+ - **Pages:** the order page, the return page and `TicketCheckout` on event pages; the check-in page is optional.
48
+ - **Check it worked:** as for the sandboxed npm install. Tested on Node in the local test site (`EVENTUAL_PLUGIN_MODE=native`, `npm run test:integration:suite` and `npm run test:e2e`).
49
+
50
+ ### Permissions and why
51
+
52
+ | Permission | Why |
53
+ | :--- | :--- |
54
+ | `content:read` | Read the event (title, dates, published or not) an order is for |
55
+ | `email:send` | Confirmations, instructions, refund notices, ticket links |
56
+ | `network:request` to `api.stripe.com` | Create Checkout sessions, check session status, and issue refunds |
57
+ | `network:request` to `challenges.cloudflare.com` | Check Cloudflare Turnstile tokens, only when you turn Turnstile on |
58
+ | `network:request` to `cdn.jsdelivr.net` | Download the admin and email translations (language packs from `@vermeulen.solutions/eventual-locales`); each file is checked against a hash built into the plugin |
59
+ | `network:request` to `licences.eventual.rsvp` | Turn your licence code into a key for this site, and free the site again. Sends only the code and the site's hostname: when an administrator clicks Activate, Refresh or Deactivate, and once a day around the end of the licence's paid year, to pick up the renewed key |
60
+
61
+ The licence check itself uses Web Crypto inside the worker and makes no network requests.
62
+
63
+ #### Public routes
64
+
65
+ The plugin declares the following public routes (listed in `trust-contract.json`):
66
+
67
+ | Route | Method | Purpose |
68
+ | :--- | :--- | :--- |
69
+ | `availability` | GET | Check ticket availability and pricing tiers for an event occurrence |
70
+ | `orders/download` | GET | Single-use download link for orders CSV generated by the admin panel |
71
+ | `checkout` | POST | Create an order and initiate Stripe Checkout (or confirm free order) |
72
+ | `checkout/release` | POST | Release held seats when a buyer abandons checkout |
73
+ | `checkout/status` | POST | Check Stripe payment status after returning from Checkout |
74
+ | `order` | POST | Look up order details and ticket codes using a signed HMAC token |
75
+ | `stripe/webhook` | POST | Webhook receiver for Stripe checkout completion and expiration events |
76
+
77
+ **Installed from the registry?** The plugin then has an ID like `r_…` instead of its name. The plugin's admin page shows the exact paths; pass them to the site components (`apiEndpoint` / `apiBase`).
78
+
79
+ **Spam protection (optional).** Every form has a hidden honeypot and a timing check. When Turnstile is off, Tickets refuses more than **10 orders from one IP address in 10 minutes** (keeping only a 32-hex keyed hash `ipHash`, never the raw IP). Off Cloudflare, EmDash knows the visitor's IP only when its trusted proxy headers are configured; without an IP there is no limit. When Turnstile is on, Cloudflare's challenge handles rate abuse and the IP check is skipped. To turn Turnstile on, paste a Cloudflare Turnstile site key and secret key on the **Tickets** admin page; the checkout form then shows Cloudflare's check. No encryption key is needed for it. If your site sends a Content Security Policy, allow `https://challenges.cloudflare.com` in `script-src` and `frame-src`. Details: the [Attendance README](https://github.com/Vermeulen-Solutions/eventual-attendance#spam-protection-optional-cloudflare-turnstile).
80
+
81
+ ## Setup
82
+
83
+ 1. **Encryption key (card payments only).** Free tickets and pay-by-invoice need no setup. To take card payments, Tickets keeps your Stripe keys as encrypted settings, so the site needs `EMDASH_ENCRYPTION_KEY` (`npx emdash secrets generate`; on Cloudflare `wrangler secret put EMDASH_ENCRYPTION_KEY`). The Tickets page tells you when it's missing.
84
+ 2. Open **Tickets** in the admin. It shows the webhook URL and the Stripe events to subscribe to.
85
+ 3. In the settings form under **Events**, paste your Stripe secret key (`sk_test_…` while testing) and the webhook signing secret (`whsec_…`).
86
+ 4. Add the components to your site:
87
+ - on the event page: `<TicketCheckout eventId={id} occurrenceStart={date} />` from `@vermeulen.solutions/eventual-tickets/astro/TicketCheckout.astro`
88
+ - on the return page (default path `/tickets/checkout`, set on the Tickets page): `<TicketCheckoutReturn backHref="/events" />`
89
+ - on the order page the ticket email links to (default `/tickets/order`): `<TicketOrder />`
90
+ - on the door staff's page (`/tickets/check`, opened while signed in): `<TicketCheckin eventId={id} occurrenceStart={date} />`
91
+
92
+ Props: `TicketCheckout` takes `eventId`, `occurrenceStart`, `apiBase` (default `/_emdash/api/plugins/eventual-tickets`; use the path the admin page shows on a registry install), `title` and `class`. `TicketCheckoutReturn` takes `apiBase`, `backHref` (default `/`) and `class`. `TicketOrder` takes `apiBase`, `checkinPath` (default `/tickets/check`; the QR opens it) and `class`. `TicketCheckin` takes `apiBase`, optional `eventId` / `occurrenceStart` (to refuse other events' tickets) and `class`.
93
+
94
+ ### Event dates
95
+
96
+ Tickets reads the event's own date, not only the date a form passes. It uses Core's `start`/`end`/`all_day`/`timezone`; for collections that keep `start_date`/`end_date` instead (camps, school series) it falls back to those, and a bare date there is an all-day event. That date is stored on the order (`startsAt`, `allDay`) and used in ticket and invoice emails, the Orders page and CSV, retention (counting from the event, not the order) and the delete guard. The seat ledger key is unchanged: `occurrenceStart` stays whatever the form sends, so existing ledgers keep working.
97
+
98
+ ### Languages
99
+
100
+ Tickets speaks English (built in), French, German, Dutch, Spanish, Italian, Portuguese (Portugal and Brazil), Polish, Czech, Swedish, Danish, Norwegian Bokmål, Finnish, Japanese, Simplified Chinese, Korean, Turkish, Ukrainian, Arabic and Hebrew. Arabic and Hebrew are laid out right to left.
101
+
102
+ `TicketCheckout`, `TicketCheckoutReturn`, `TicketOrder` and `TicketCheckin` take a `locale` such as `"fr"`, `"de"` or `"pt-BR"` (default `"en"`); an unknown language falls back to the closest one, then English. The checkout sends its language with the order, and every buyer email for that order (ticket, invoice instructions, automatic-refund notice) is written in it; the order page switches to the order's language when it differs from the page's own. Prices and dates are formatted for the language (Swiss francs in the Swiss style). The components' texts come from the `@vermeulen.solutions/eventual-locales` npm package, installed with Tickets; each language is its own chunk, so a page loads only the one it shows.
103
+
104
+ The admin pages, the editor panel and the Orders page follow the admin UI's language. The admin and email texts are not in the plugin bundle: English is built in, and the other languages are language packs the plugin downloads from `cdn.jsdelivr.net` (the public npm CDN) and keeps in its storage. A background task fetches them, the site's own language first, within about five minutes of installing or updating; until a language has arrived, its emails and admin pages are in English. Each pack is checked against a hash built into this release, so a release always uses exactly the translations it was tested with. The check-in verdicts an organiser sees and the organiser-facing emails are English. The plugin's sidebar labels come from the English manifest and stay English.
105
+
106
+
107
+ ### Theming
108
+
109
+ The components' CSS is global and prefixed (`.eventual-tickets-widget`, `.et-*`, `.etr-*`, `.eto-*`, `.etc-*`). Theme them by setting the `--et-*` variables on a wrapper around the component: colours `--et-primary`, `--et-primary-hover`, `--et-on-primary`, `--et-bg`, `--et-field-bg` (inputs and dropdowns; default white), `--et-border`, `--et-text`, `--et-muted`, `--et-success(-bg)`, `--et-warning(-bg)`, `--et-error(-bg)`, `--et-info-bg`; shapes `--et-radius` (cards), `--et-card-max-width` (default 480px; `none` fills the column), `--et-control-radius` (fields and messages), `--et-button-radius`; focus `--et-focus`, `--et-focus-ring`. Set a field's text and `--et-field-bg` together for a dark theme: the component paints both, so a light text colour alone would sit on a white field. Site CSS can also override a class directly (for example `.et-submit { text-transform: uppercase; }`), but variables survive a redesign and are the supported way.
110
+
111
+ ### After booking
112
+
113
+ When a free or invoice order is confirmed, the checkout page shows the buyer their **order reference** (`ord_…`) and, when the server could build it within the call budget, a link to their **order page** ("View your tickets" / "View or cancel your order"). The link is the same one the email carries, with the token in the URL fragment; it appears only in that browser's own answer, never in a GET request or query string. If it can't be built, the reference still is, and the page's texts only promise what it shows. Card buyers get the reference on the return page after paying; their order link comes by email.
114
+
115
+ ### Orders and automatic refunds
116
+
117
+ - View, filter and export orders (including `Local time` in the event's time zone) on the **Orders** admin page.
118
+ - Paid card orders that arrived after seats sold out (`PAID_NO_SEATS`) or paid a mismatched amount/currency (`AMOUNT_MISMATCH`) get no ticket. With **Refund paid orders that got no seats, or paid the wrong amount, automatically** turned on (default `true` on the **Tickets** admin page), the `tickets-auto-refund` background task runs every 5 minutes, requests a full Stripe refund once (using the order's idempotency key), emails the buyer, and waits for Stripe's `charge.refunded` webhook; the Orders banner marks those orders as **refund pending**.
119
+
120
+ ### Invoices
121
+
122
+ Turn on **Offer payment by invoice or bank transfer** in the event's Tickets panel and write the payment instructions. Checkout confirms the seats at once and emails the bank details, the order reference and a link to the order page where the buyer can cancel. The email always comes first: on a budget-tight cold start it may arrive without the link rather than not at all. The organiser marks the invoice paid on the **Orders** page; that issues the ticket.
123
+
124
+ ### Activating your licence
125
+
126
+ - **Activate:** buying a licence emails you a licence code (`EVT-XXXX-XXXX-XXXX-XXXX`). On the **Tickets** admin page, paste it into **Licence** and click **Activate**. Tickets sends the code and this site's hostname to `licences.eventual.rsvp` once, and stores the key it gets back (for the hostname and its `www` twin) in plugin KV; no `EMDASH_ENCRYPTION_KEY` is needed. From then on the key is checked offline.
127
+ - **More sites:** paste the same code on each site, up to the number of sites the licence covers (one, ten, or any number).
128
+ - **Moving a site:** click **Deactivate this site** on the old site, then activate the new one. Moves are limited to 3 per site a year, because old keys keep working offline. A copied site with the code saved shows **Activate this site**.
129
+ - **Refresh key:** asks for a fresh key for this site without using another place on the licence.
130
+ - **Your sites:** `https://licences.eventual.rsvp/manage` lists every site that uses your code, with a Deactivate button for each, so a site you can no longer reach can be freed too.
131
+ - **Lost code:** `https://licences.eventual.rsvp/resend` emails your codes to the address you bought with.
132
+ - **A key instead of a code:** a key issued by hand (`evt1.…`) can be pasted into the same field.
133
+ - **What the page shows when the key is valid:** a banner "Licensed for <hostname> (updates until <date>)." and the line "Licence: active for <hostname> (updates until <date>)."
134
+ - **`LICENCE_REQUIRED`:** this hostname has no key. Paste your licence code and click Activate. Visitors see "Ticket sales are not available on this site right now."; existing orders keep working.
135
+ - **`LICENCE_EXPIRED`:** the key covers versions released until a date, and this build is newer. Renew, or reinstall a version released during the paid year. The page says which date.
136
+ - **Invalid for this hostname:** the key is valid but was issued for another hostname, for example after the site moved. Click **Activate this site**. The page says "Invalid licence key for <hostname>".
137
+ - **Which hosts are free:** development and preview hosts (`localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, `*.workers.dev`, `*.pages.dev`) need no key and can always sell tickets.
138
+ - **Nothing already sold breaks:** existing orders, Stripe webhooks, refunds, buyer order pages, QR check-in, retention and the Orders page are never blocked.
139
+
140
+ ### Deleting events
141
+
142
+ An event with upcoming orders (paid, pending or awaiting an invoice payment) can't be moved to the trash. Cancel them first, or unpublish the event to stop new bookings.
143
+
144
+ ### Personal data
145
+
146
+ Buyer names, email addresses and answers are personal data. Under privacy frameworks (such as Swiss FADP and EU GDPR), organisers are expected to retain personal details only as long as necessary and erase them on request.
147
+
148
+ - **Automated retention:** The `tickets-retention` task runs hourly (at minute 23) and erases buyer names, email addresses and registration answers for orders whose event passed more than `retentionDays` ago (configurable on the Tickets admin page, default 365 days, 30–3650). Bookings for an event without a date are kept while they're live; the period starts again each time the sweep finds one. Financial and bookkeeping fields (amounts, currency, tier lines, order status, Stripe IDs) are preserved for statutory accounting obligations.
149
+ - **Erasure on request:** On the **Orders** admin page, the **Personal data** section allows looking up orders by buyer email address. One click erases buyer names, email addresses and answers across past and cancelled orders. Active orders (pending, pending_invoice, or paid for upcoming/undated occurrences) are protected from erasure until cancelled or refunded. Erased orders stay in the list and CSV as "· erased" with `—` for the buyer so the money totals add up; a Refund action stays where a refund may still be owed.
150
+ - **Stripe data:** Note that Stripe keeps its own independent record of customer and transaction data according to its own privacy policy and financial regulations; erasing data in EmDash erases local CMS records only.
151
+ - **Starting point, not legal advice:** Retention rules depend on applicable legislation, tax requirements, and your organisation's policies.
152
+
153
+ ## Large events
154
+
155
+ These are the limits of one background task or download, given the sandbox's 10 host calls. Past a limit the plugin skips the work or truncates the file rather than failing.
156
+
157
+ | What | Limit | What happens past it | What to do |
158
+ | :--- | :--- | :--- | :--- |
159
+ | Reconcile one date | ≈ 700 orders | The date is skipped (`TOO_LARGE`) and tried again on a later tick; its seat count is not corrected automatically | Split the event by date, so each date has its own pool |
160
+ | Orders CSV export | ≈ 700 orders | The file is truncated with a note at the top | Filter by event and date and export each date separately |
161
+ | Automatic refunds | 2 per 5-minute tick | Remaining refunds are requested on the following ticks | None needed |
162
+ | Hold sweep | 2 overdue holds per minute | Remaining holds are checked on the following minutes | None needed |
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ npm install # if npm 10.5 crashes ("edgesOut"), run: npm install --legacy-peer-deps && npm install
168
+ npm run validate
169
+ npm run typecheck
170
+ npm test # EmDash runtime test host; Stripe is never called
171
+ npm run build
172
+ npm run release:check # one line per release gate; never publishes
173
+ ```
174
+
175
+ `npm run release:date` sets `BUILD_DATE` (today UTC, or `--date YYYY-MM-DD`) before a release build. It changes only that line, never the licence public key.
176
+
177
+ `npm run licence:apply` builds `LICENSE` from `docs/LICENSE-DRAFT.md` and `licence-facts.json` (the owner fills in the licensor name, postal address and publication date). It refuses when a fact is missing or malformed, or when a placeholder is left; `-- --check` only reports.
178
+
179
+ `trust-contract.json` records capabilities, hosts, hooks, public routes and storage indexes; `npm test` fails when the build differs. Change it only on purpose.
180
+
181
+ `npm run release:check` runs the tests, type check, manifest check and build, then checks the manifest version, the changelog heading, what `npm pack` would contain, that every `astro/` file is self-contained, that the package is not `private`, and (Tickets) that the licence public key, build date and `LICENSE` are ready. It stops at the first failure and prints nothing but one line per step. It never publishes, tags, commits or uploads.
182
+
183
+ The shared library is a dev dependency pinned to a commit on GitHub (private repo; needs read access).
184
+
185
+ ## Troubleshooting
186
+
187
+ - **Card payments are off:** paste a Stripe test/live secret key and the webhook signing secret on the **Tickets** admin page (the setup checklist names the missing piece). Free tickets and pay-by-invoice work without any Stripe key.
188
+ - **"No valid licence key for …":** production hosts need a key whose hostnames include the site; development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) are exempt. An expired key blocks new sales only — existing orders, refunds, the webhook and check-in keep working.
189
+ - **A buyer gets no email:** EmDash needs an email provider. Without one the confirmation says so and shows the order reference and the order link; the buyer can still get their ticket from that page.
190
+ - **"Route not found" on a registry install:** use the `apiBase` shown on the Tickets page (the registry ID, not `eventual-tickets`).
191
+ - **The webhook never arrives:** the endpoint URL must be the one shown on the Tickets page, subscribed to the events listed there, and the signing secret pasted into the settings. Stripe retries for three days.
192
+ - **A public route answers `TRY_AGAIN`:** an unexpected host/database error was caught and logged; the raw message is never shown.
193
+ - **A refund seems missing:** refunds made in Stripe appear after Stripe's `charge.refunded` webhook; the Orders page shows "part refunded" for partial amounts. Automatic refunds run every five minutes when the toggle is on.
194
+
195
+ ## Licence
196
+
197
+ Eventual RSVP – Tickets is commercial software. See [LICENSE](LICENSE) for the terms. Third-party components and their licences are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
198
+
199
+ - Development and preview hosts (`localhost`, `*.workers.dev`, `*.pages.dev`) need no key.
200
+ - Every other site needs a licence: USD 30 per site per year, USD 100 per year for up to ten sites, USD 200 per year for any number of sites.
201
+ - A licensed site keeps every version released during the paid year, for ever.
202
+
203
+ Licences are bought on the suite's website.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  /** @type {import("emdash").PluginDescriptor} */
8
8
  const descriptor = Object.freeze({
9
9
  "id": "eventual-tickets",
10
- "version": "0.1.0",
10
+ "version": "0.1.1",
11
11
  "format": "standard",
12
12
  "entrypoint": "@vermeulen.solutions/eventual-tickets/sandbox",
13
13
  "capabilities": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "eventual-tickets",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "declaredAccess": {
5
5
  "content": {
6
6
  "read": {}
package/dist/plugin.mjs CHANGED
@@ -1,4 +1,4 @@
1
- function e(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}}function t(e){return e}var n=class extends Error{code=`RPC_BUDGET_EXCEEDED`;constructor(){super(`RPC budget exhausted for this invocation.`)}},r=class{limit;used=0;constructor(e=10){this.limit=e}get remaining(){return this.limit-this.used}has(e){return this.remaining>=e}async call(e){if(this.used>=this.limit)throw new n;return this.used++,e()}},i=class{parent;reserve;constructor(e,t){this.parent=e,this.reserve=t}get remaining(){return this.parent.remaining-this.reserve}has(e){return this.remaining>=e}call(e){return this.remaining<=0?Promise.reject(new n):this.parent.call(e)}};function a(e,t,n,r,i){let a=t?.[n];typeof a!=`function`||!e.has(1)||e.call(async()=>{await a.call(t,r,i)}).catch(()=>{})}function o(e){return typeof e==`string`?e.trim():``}function s(e){return/^\d{4}-\d{2}-\d{2}$/.test(e)&&!Number.isNaN(Date.parse(`${e}T00:00:00Z`))}function c(e,t){let n=new Date(`${e}T00:00:00Z`);return n.setUTCDate(n.getUTCDate()+t),n.toISOString().slice(0,10)}function l(e,t=``){let n=o(e.timezone)||void 0;if(e.all_day===!0){let r=o(e.start_date)||o(e.start).slice(0,10),i=t?t.slice(0,10):r;if(!s(i))return null;let a=o(e.end_date),l=s(r)&&s(a)?Math.max(0,Math.round((Date.parse(a)-Date.parse(r))/864e5)):0;return{startsAt:`${i}T00:00:00.000Z`,allDay:!0,startDate:i,end:c(i,l),timezone:n}}let r=o(e.start)||o(e.start_date);if(!o(e.start)&&s(r))return l({...e,all_day:!0},t);let i=Date.parse(r),a=t?Date.parse(t):i;if(Number.isNaN(a))return null;let u=Date.parse(o(e.end)||o(e.end_date)),d=!Number.isNaN(i)&&!Number.isNaN(u)&&u>i?u-i:36e5;return{startsAt:new Date(a).toISOString(),allDay:!1,end:new Date(a+d).toISOString(),timezone:n}}function u(e){if(typeof e!=`string`)return;let t=e.trim();if(!(!t||t.length>100))try{return new Intl.DateTimeFormat(`en`,{timeZone:t}),t}catch{return}}function d(e,t,n){let r=String(e??``).trim();if(!r)return``;let i=new Date(r);if(Number.isNaN(i.getTime()))return r;let a=u(t),o=a??`UTC`,s=String(n??``).trim()||`en`,c=new Intl.DateTimeFormat(s,{timeZone:o,dateStyle:`medium`,timeStyle:`short`}).format(i);if(!a)return`${c} UTC`;let l=new Intl.DateTimeFormat(s,{timeZone:a,timeZoneName:`short`}).formatToParts(i).find(e=>e.type===`timeZoneName`)?.value;return l?`${c} ${l}`:`${c} UTC`}const f=/^[a-z0-9_]{1,48}$/,p=`ledger:`;function m(e,t=``){let n=e.trim(),r=t.trim();return r?`${p}${n}:${r}`:`${p}${n}`}function h(e,t){let n=e.slice(7);return t&&n.endsWith(`:${t}`)?n.slice(0,-(t.length+1)):n}function g(e,t=``,n=``){return{capacity:e,poolKey:n,taken:0,held:0,waitlistCount:0,nextPosition:1,occurrenceStart:t,updatedAt:new Date().toISOString()}}function _(e,t=0){if(!e||typeof e!=`object`)return null;let n=e=>typeof e==`number`&&Number.isFinite(e)&&e>=0?Math.floor(e):0,r=u(e.timezone);return{capacity:typeof e.capacity==`number`?n(e.capacity):t,taken:n(e.taken),held:n(e.held),waitlistCount:n(e.waitlistCount),nextPosition:Math.max(1,n(e.nextPosition)),poolKey:typeof e.poolKey==`string`?e.poolKey:``,occurrenceStart:typeof e.occurrenceStart==`string`?e.occurrenceStart:``,...typeof e.label==`string`&&e.label?{label:e.label.slice(0,200)}:{},...r?{timezone:r}:{},...v(e.buckets,n),updatedAt:typeof e.updatedAt==`string`?e.updatedAt:new Date().toISOString()}}function v(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return{};let n={};for(let[r,i]of Object.entries(e).slice(0,100)){if(!f.test(r)||!i||typeof i!=`object`)continue;let e=i;n[r]={capacity:t(e.capacity),taken:t(e.taken),held:t(e.held)}}return Object.keys(n).length?{buckets:n}:{}}function y(e,t,n){let r=e.buckets?.[t],i=n??r?.capacity??0;return i===0?1/0:Math.max(0,i-(r?.taken??0)-(r?.held??0))}function b(e){return e.capacity===0?1/0:Math.max(0,e.capacity-e.taken-e.held)}async function x(e,t,n,r,i=2){for(let a=0;a<i&&t.has(2);a++){let i=await t.call(()=>e.getVersioned(n)),a=r(i?_(i.value):null);if(`abort`in a)return{applied:!1,result:a.abort,reason:`ABORTED`};let o={...a.next,updatedAt:new Date().toISOString()};if((await t.call(()=>e.compareAndSet(n,i?.revision??null,o))).applied)return{applied:!0,result:a.result,ledger:o}}return{applied:!1,reason:`CONFLICT`}}function S(e,t){if(!t)return e;let n={...e??{}};for(let[e,r]of Object.entries(t)){let t=n[e];if(!t)continue;let i=Math.min(r.heldToTaken??0,t.held);n[e]={capacity:t.capacity,taken:Math.max(0,t.taken-(r.taken??0))+i,held:Math.max(0,t.held-(r.held??0)-i)}}return n}async function C(e,t,n,r,i=2){let a=await x(e,t,n,e=>{if(!e)return{abort:!0};let t=Math.min(r.heldToTaken??0,e.held);return{next:{...e,taken:Math.max(0,e.taken-(r.taken??0))+t,held:Math.max(0,e.held-(r.held??0)-t),waitlistCount:Math.max(0,e.waitlistCount-(r.waitlistCount??0)),...e.buckets||r.buckets?{buckets:S(e.buckets,r.buckets)}:{}},result:!0}},i);return a.applied||a.reason===`ABORTED`}const w=/^[a-z0-9_]{1,48}$/;async function T(e,t,n,r){let{capacity:i,lines:a,poolKey:o=``,occurrenceStart:s=``,label:c,timezone:l,confirm:d=!1,maxAttempts:f=2}=r,p=u(l),m=new Set;for(let e of a){if(!(w.test(e.bucket)&&!m.has(e.bucket)&&Number.isSafeInteger(e.quantity)&&e.quantity>=1&&Number.isSafeInteger(e.bucketCapacity)&&e.bucketCapacity>=0))return{ok:!1,reason:`INVALID`,bucket:e.bucket};m.add(e.bucket)}if(a.length===0||!Number.isSafeInteger(i)||i<0)return{ok:!1,reason:`INVALID`};let h=a.reduce((e,t)=>e+t.quantity,0),_=await x(e,t,n,e=>{let t={...e??g(i,s,o),capacity:i,poolKey:e?.poolKey||o,...c?{label:c.slice(0,200)}:{},...p?{timezone:p}:{}};if(b(t)<h)return{abort:{ok:!1,reason:`FULL`}};for(let e of a)if(y(t,e.bucket,e.bucketCapacity)<e.quantity)return{abort:{ok:!1,reason:`BUCKET_FULL`,bucket:e.bucket}};let n={...t.buckets??{}};for(let e of a){let t=n[e.bucket]??{capacity:e.bucketCapacity,taken:0,held:0};n[e.bucket]={capacity:e.bucketCapacity,taken:t.taken+(d?e.quantity:0),held:t.held+(d?0:e.quantity)}}return{next:{...t,taken:t.taken+(d?h:0),held:t.held+(d?0:h),buckets:n},result:{ok:!0}}},f);return _.applied?{ok:!0,ledger:_.ledger}:_.reason===`ABORTED`&&_.result&&!_.result.ok?{ok:!1,reason:_.result.reason,..._.result.bucket?{bucket:_.result.bucket}:{}}:{ok:!1,reason:`BUSY`}}async function E(e,t,n,r=2){let i;do{if(!e.has(r))return`TOO_LARGE`;let a=await e.call(()=>t(i));for(let{data:e}of a.items)if(n(e)===!1)return`BUSY`;i=a.hasMore?a.cursor:void 0}while(i);return`OK`}function D(e,t){let n=new Set([...Object.keys(e??{}),...Object.keys(t??{})]);for(let r of n){let n=e?.[r],i=t?.[r];if((n?.taken??0)!==(i?.taken??0)||(n?.held??0)!==(i?.held??0))return!1}return!0}async function O(e,t,n,r,o={}){let s=o.nowMs??Date.now(),c=o.quietMs??6e5,l=e=>{let t=e?Date.parse(e):NaN;return!Number.isFinite(t)||s-t>=c},u=await t.call(()=>e.getVersioned(n)),d=u?_(u.value):null;if(!u||!d)return{status:`MISSING`,key:n};if(!l(d.updatedAt))return{status:`BUSY`,key:n};let f=d.poolKey||h(n,d.occurrenceStart),p=await r({ledger:d,poolKey:f,occurrenceStart:d.occurrenceStart,budget:new i(t,1),isQuiet:l});if(p===`BUSY`||p===`TOO_LARGE`)return{status:p,key:n};let m;if(d.buckets||p.buckets){m={};for(let e of new Set([...Object.keys(d.buckets??{}),...Object.keys(p.buckets??{})]))m[e]={capacity:d.buckets?.[e]?.capacity??0,taken:p.buckets?.[e]?.taken??0,held:p.buckets?.[e]?.held??0}}let g={...d,poolKey:f,taken:p.taken,held:p.held,waitlistCount:p.waitlistCount??d.waitlistCount,nextPosition:Math.max(d.nextPosition,(p.maxPosition??0)+1),...m?{buckets:m}:{}};return g.taken===d.taken&&g.held===d.held&&g.waitlistCount===d.waitlistCount&&g.nextPosition===d.nextPosition&&g.poolKey===d.poolKey&&D(g.buckets,d.buckets)?{status:`UNCHANGED`,key:n}:(await t.call(()=>e.compareAndSet(n,u.revision,{...g,updatedAt:new Date(s).toISOString()}))).applied?(a(t,o.log,`warn`,`Seat ledger corrected from records`,{key:n,before:d,after:g}),{status:`CORRECTED`,key:n,before:d,after:g}):{status:`CONFLICT`,key:n}}async function k(e,t,n,r){let a=(await t.call(()=>e.list(p))).map(e=>e.key).sort();if(a.length===0)return null;let o=await t.call(()=>e.getVersioned(n)),s=a.find(e=>!o||e>o.value)??a[0],c=await r(s,new i(t,1));return await t.call(()=>e.compareAndSet(n,o?.revision??null,s)),c}const A=`state:schedules`;let j=null;function M(e){return e.map(e=>`${e.name}=${e.schedule}`).sort().join(`;`)}function N(e){return typeof e==`object`&&!!e&&e.v===1&&typeof e.fingerprint==`string`&&Array.isArray(e.done)}async function P(e,t,n,r){let i=M(r);if(j===i)return`OK`;if(!t||!n.has(1))return`SKIPPED`;let a=await n.call(()=>e.getVersioned(A));if(a?.value===i)return j=i,`OK`;let o=new Set(N(a?.value)&&a.value.fingerprint===i?a.value.done:[]),s=r.filter(e=>!o.has(`${e.name}=${e.schedule}`)),c=n.remaining-1;if(c<1)return`SKIPPED`;let l=s.slice(0,c);for(let e of l)await n.call(()=>t.schedule(e.name,{schedule:e.schedule})),o.add(`${e.name}=${e.schedule}`);let u=r.every(e=>o.has(`${e.name}=${e.schedule}`)),d=u?i:{v:1,fingerprint:i,done:[...o]};return await n.call(()=>e.compareAndSet(A,a?.revision??null,d)),u?(j=i,`SCHEDULED`):`PARTIAL`}function F(e){return Array.from(new Uint8Array(e),e=>e.toString(16).padStart(2,`0`)).join(``)}async function I(e,t,n){if(!e||e.length<32)throw Error(`HMAC secret is missing or too short.`);if(!/^[a-z][a-z0-9-]{0,31}$/.test(t))throw Error(`Token purpose must be a short lowercase word.`);let r=await crypto.subtle.importKey(`raw`,new TextEncoder().encode(e),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]),i=`${t}:${n.join(`:`)}`;return F(await crypto.subtle.sign(`HMAC`,r,new TextEncoder().encode(i)))}async function L(e){return F(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e)))}function ee(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}function R(e){let t=new Uint8Array(e);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,`0`)).join(``)}function z(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}async function B(e,t,n=`state:signing-key`){for(let r=0;r<2;r++){let r=await t.call(()=>e.getVersioned(n));if(r&&z(r.value))return r.value;let i=R(32);if((await t.call(()=>e.compareAndSet(n,r?.revision??null,i))).applied)return i}throw Error(`Unable to initialise the signing secret.`)}function te(e){return/ENCRYPTION_KEY|PluginSettingEncryption/i.test(String(e))}function ne(e,t){let n=typeof e==`string`?e.trim():``;return!n.startsWith(`/`)||n.startsWith(`//`)||/[\s#?\\]/.test(n)?t:n.slice(0,200)}function re(e,t=`events`){let n=typeof e==`string`?e.trim():``;return/^[a-z][a-z0-9_]{0,63}$/.test(n)?n:t}const ie=/^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,}$/;function ae(e){return typeof e==`string`&&e.length<=320&&ie.test(e)}function oe(e){let t=typeof e==`string`?e.trim().toLowerCase():``;return ae(t)?t:``}function se(e){return typeof e==`string`&&/^[A-Za-z0-9_-]{16,64}$/.test(e)}function ce(e){return!e||e.trim().length===0}function le(e,t=Date.now()){if(!e||typeof e!=`number`)return!0;let n=t-e;return n>=1500&&n<=72e5}function V(e){let t=e.data?.inventory_mode;return t===`rsvp`||t===`tickets`?t:null}function ue(e){return V(e)===`tickets`}function de(e){return(e.translationGroup||e.id).trim()}Object.freeze([{slug:`inventory_mode`,label:`Seats managed by`,type:`select`,translatable:!1,validation:{options:[`rsvp`,`tickets`]}},{slug:`capacity`,label:`Capacity per date (0 = unlimited)`,type:`integer`,translatable:!1,defaultValue:0},{slug:`allow_waitlist`,label:`Allow waitlist when full`,type:`boolean`,translatable:!1,defaultValue:!1}]);const H=[`text`,`textarea`,`email`,`phone`,`number`,`date`,`checkbox`,`select`],U=Object.freeze({questions:20,label:400,options:30,option:80,text:500,textarea:2e3});function W(e){return e.toLowerCase().normalize(`NFKD`).replace(/[̀-ͯ]/g,``).replace(/[^a-z0-9]+/g,`_`).replace(/^_+|_+$/g,``).slice(0,40)||`question`}function G(e,t){let n=e;for(let r=2;t.has(n);r++)n=`${e}_${r}`;return t.add(n),n}function fe(e){let t=[],n=[],r=[],i=new Set;return(e??``).split(/\r\n|\r|\n/).forEach((e,a)=>{let o=e.trim();if(!o||o.startsWith(`#`))return;let s=a+1,c=`Line ${s}`,[l=``,u=``,d=``,f=``]=o.split(`|`).map(e=>e.trim());if(!l){n.push(`${c}: start with the question's label.`),r.push({line:s,code:`NO_LABEL`});return}if(l.length>U.label){n.push(`${c}: the label is longer than ${U.label} characters.`),r.push({line:s,code:`LABEL_TOO_LONG`,limit:U.label});return}let p=u.toLowerCase()||`text`;if(!H.includes(p)){n.push(`${c}: "${u}" isn't a type. Use one of: ${H.join(`, `)}.`),r.push({line:s,code:`BAD_TYPE`,value:u});return}let m=d.toLowerCase();if(m&&m!==`required`&&m!==`optional`){n.push(`${c}: write "required" or "optional" in the third column.`),r.push({line:s,code:`BAD_REQUIRED`});return}let h={id:G(W(l),i),label:l,type:p,required:m===`required`};if(p===`select`){let e=[...new Set(f.split(`,`).map(e=>e.trim()).filter(Boolean))];if(e.length===0){n.push(`${c}: a select question needs options, for example "S, M, L".`),r.push({line:s,code:`NO_OPTIONS`});return}if(e.length>U.options){n.push(`${c}: at most ${U.options} options.`),r.push({line:s,code:`TOO_MANY_OPTIONS`,limit:U.options});return}if(e.some(e=>e.length>U.option)){n.push(`${c}: options can be at most ${U.option} characters.`),r.push({line:s,code:`OPTION_TOO_LONG`,limit:U.option});return}h.options=e}else if(f){n.push(`${c}: only select questions take options.`),r.push({line:s,code:`OPTIONS_NOT_ALLOWED`});return}t.push(h)}),t.length>U.questions&&(n.push(`At most ${U.questions} questions per event.`),r.push({line:null,code:`TOO_MANY_QUESTIONS`,limit:U.questions})),{questions:t.slice(0,U.questions),errors:n,issues:r}}function pe(e){if(!Array.isArray(e))return[];let t=new Set,n=[];for(let r of e.slice(0,U.questions)){if(!r||typeof r!=`object`)continue;let e=r,i=typeof e.label==`string`?e.label.trim().slice(0,U.label):``,a=H.includes(e.type)?e.type:null;if(!i||!a)continue;let o=typeof e.id==`string`&&/^[a-z0-9_]{1,48}$/.test(e.id)&&!t.has(e.id)?(t.add(e.id),e.id):G(W(i),t),s=a===`select`&&Array.isArray(e.options)?e.options.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>e.trim().slice(0,U.option)).slice(0,U.options):void 0;a===`select`&&!s?.length||n.push({id:o,label:i,type:a,required:e.required===!0,...s?{options:s}:{}})}return n}const K=/^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,}$/,me=/^\+?[0-9 ()./-]{5,30}$/;function he(e,t){let n=t&&typeof t==`object`&&!Array.isArray(t)?t:{},r=[],i={};for(let t of e){let e=n[t.id];if(t.type===`checkbox`){let n=e===!0||e===`true`||e===`on`;t.required&&!n?i[t.id]=`REQUIRED`:r.push({id:t.id,label:t.label,value:n});continue}let a=typeof e==`string`?e.trim():typeof e==`number`&&Number.isFinite(e)?String(e):``;if(!a){t.required&&(i[t.id]=`REQUIRED`);continue}let o=t.type===`textarea`?U.textarea:U.text;if(a.length>o){i[t.id]=`TOO_LONG`;continue}let s=a,c=!0;switch(t.type){case`email`:c=K.test(a),s=a.toLowerCase();break;case`phone`:c=me.test(a);break;case`number`:s=Number(a),c=Number.isFinite(s);break;case`date`:c=/^\d{4}-\d{2}-\d{2}$/.test(a)&&!Number.isNaN(Date.parse(`${a}T00:00:00Z`));break;case`select`:c=t.options?.includes(a)??!1;break;case`text`:c=!/[\r\n]/.test(a);break;default:break}c?r.push({id:t.id,label:t.label,value:s}):i[t.id]=`INVALID`}return Object.keys(i).length?{ok:!1,errors:i}:{ok:!0,answers:r}}function ge(e){return(e??[]).map(e=>`${e.label}: ${typeof e.value==`boolean`?e.value?`Yes`:`No`:e.value}`).join(`; `)}const _e=`challenges.cloudflare.com`,ve=`https://${_e}/turnstile/v0/siteverify`;`${_e}`;const ye=/^[A-Za-z0-9_-]{10,100}$/;function be(e){let t=typeof e==`string`?e.trim():``;return ye.test(t)?t:``}function xe(e){return typeof e==`string`&&e.length>0&&e.length<=2048&&!/\s/.test(e)}async function Se(e,t,n){if(!xe(n.token))return{ok:!1,reason:`missing-input-response`};let r=new URLSearchParams({secret:n.secret,response:n.token});n.remoteIp&&r.set(`remoteip`,n.remoteIp);let i;try{i=await t.call(()=>e(ve,{method:`POST`,headers:{"content-type":`application/x-www-form-urlencoded`},body:r.toString()}))}catch(e){if(e?.code===`RPC_BUDGET_EXCEEDED`)throw e;return{ok:!0,verified:!1,reason:`unreachable`}}if(i.status>=500)return{ok:!0,verified:!1,reason:`http-${i.status}`};let a=await i.json().catch(()=>null);if(a?.success===!0)return{ok:!0,verified:!0};let o=Array.isArray(a?.[`error-codes`])?a[`error-codes`].map(String):[];return o.includes(`internal-error`)?{ok:!0,verified:!1,reason:`internal-error`}:{ok:!1,reason:o[0]??(a?`rejected`:`bad-response`)}}const q=`eventual-tickets`,Ce=`stripeSecretKey`,we=`stripeWebhookSecret`,Te=[`checkout.session.completed`,`checkout.session.expired`,`checkout.session.async_payment_succeeded`,`checkout.session.async_payment_failed`,`charge.refunded`];function J(e){if(typeof e!=`string`||!e.trim())return`missing`;let t=e.trim();return t.startsWith(`sk_test_`)?`test`:t.startsWith(`sk_live_`)?`live`:t.startsWith(`rk_test_`)?`restricted-test`:t.startsWith(`rk_live_`)?`restricted-live`:`unrecognised`}function Ee(e){return e===`live`||e===`restricted-live`}function De(e,t=q){let n=String(e??``).replace(/\/+$/,``);return/^https?:\/\//.test(n)?`${n}/_emdash/api/plugins/${t||q}/stripe/webhook`:null}async function Oe(e,t,n){let r=String(e.site?.url??``).replace(/\/+$/,``),i=De(r,n||e.plugin?.id||q);try{let n=await t.call(()=>e.settings.get(Ce)),a=await t.call(()=>e.settings.get(we));!n&&!a&&await t.call(()=>e.settings.compareAndSet(we,`eventual-tickets:encryption-probe`,`probe`));let o=J(n),s=typeof a==`string`&&a.trim().startsWith(`whsec_`);return{encryption:!0,stripeKey:o,webhookSecret:s,siteUrl:r,webhookUrl:i,ready:o!==`missing`&&o!==`unrecognised`&&s&&i!==null}}catch(e){if(!te(e))throw e;return{encryption:!1,stripeKey:`missing`,webhookSecret:!1,siteUrl:r,webhookUrl:i,ready:!1}}}function ke(e){if(typeof e!=`string`)return;let t=e.toUpperCase().replace(/[\s-]+/g,``);if(t.startsWith(`EVT`)&&(t=t.slice(3).replace(/O/g,`0`).replace(/[IL]/g,`1`),!(t.length!==16||![...t].every(e=>`0123456789ABCDEFGHJKMNPQRSTVWXYZ`.includes(e)))))return`EVT-${t.slice(0,4)}-${t.slice(4,8)}-${t.slice(8,12)}-${t.slice(12,16)}`}function Ae(e){let t=e;if(!(!t||typeof t!=`object`||typeof t.licence!=`string`||typeof t.seats!=`number`))return{licence:t.licence,seats:t.seats,used:typeof t.used==`number`?t.used:0,hostnames:Array.isArray(t.hostnames)?t.hostnames.filter(e=>typeof e==`string`).slice(0,100):[],updatesUntil:typeof t.updatesUntil==`string`?t.updatesUntil:``,movesLeft:typeof t.movesLeft==`number`?t.movesLeft:null}}const je=[`CODE_INVALID`,`CODE_REVOKED`,`HOSTNAME_INVALID`,`NO_FREE_SITE`,`NOT_ACTIVE`,`MOVES_EXHAUSTED`,`TOO_MANY_REQUESTS`];async function Me(e,t,n,r,i){if(!e)return{ok:!1,error:`UNREACHABLE`};let a;try{a=await t.call(()=>e(`https://licences.eventual.rsvp/v1/${n}`,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`},body:JSON.stringify({code:r,hostname:i})}))}catch{return{ok:!1,error:`UNREACHABLE`}}if(a.status===429)return{ok:!1,error:`TOO_MANY_REQUESTS`};let o;try{o=await a.json()}catch{return{ok:!1,error:`UNREACHABLE`}}let s=Ae(o?.status);if(o?.ok===!0){let e=typeof o.key==`string`&&o.key.startsWith(`evt1.`)?o.key:void 0;return n===`activate`&&!e&&o.exempt!==!0?{ok:!1,error:`UNREACHABLE`}:{ok:!0,...e?{key:e}:{},...o.exempt===!0?{exempt:!0}:{},...s?{status:s}:{}}}return{ok:!1,error:je.includes(o?.error)?o.error:`UNREACHABLE`,...s?{status:s}:{}}}const Ne=`evt1`,Pe=4096,Fe=new Set([`localhost`,`127.0.0.1`,`[::1]`]),Ie=[`localhost`,`workers.dev`,`pages.dev`],Le={ok:!1,error:`LICENCE_INVALID`},Re=/^[A-Za-z0-9_-]*$/;function ze(e){if(typeof e!=`string`||e===``||!Re.test(e))return;let t=e.length%4;if(t===1)return;let n=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`)+(t===0?``:`=`.repeat(4-t)),r;try{r=atob(n)}catch{return}return Uint8Array.from(r,e=>e.charCodeAt(0))}const Be=/^[A-Za-z0-9.\-:[\]]+$/;function Ve(e){if(typeof e!=`string`||!Be.test(e))return;let t;try{t=new URL(`http://${e}/`)}catch{return}let n=t.hostname.endsWith(`.`)?t.hostname.slice(0,-1):t.hostname;return n===``?void 0:n}function He(e){let t=typeof e==`string`?e.trim():``;if(t===``||!URL.canParse(t))return``;let n=new URL(t);return n.protocol!==`https:`&&n.protocol!==`http:`?``:Ve(n.hostname)??``}function Ue(e){let t=Ve(e);return t===void 0?!1:Fe.has(t)?!0:Ie.some(e=>t.endsWith(`.${e}`)&&t.length>e.length+1)}function We(e){if(typeof e!=`string`)return!1;let t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e);if(!t)return!1;let[n,r,i]=[Number(t[1]),Number(t[2]),Number(t[3])],a=new Date(Date.UTC(n,r-1,i));return a.getUTCFullYear()===n&&a.getUTCMonth()===r-1&&a.getUTCDate()===i}function Ge(e){return typeof e==`string`&&Ve(e)===e}function Ke(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(t.v!==1||t.product!==`tickets`)&&!(!Array.isArray(t.sites)||t.sites.length===0||t.sites.length>100)&&t.sites.every(Ge)&&!(typeof t.customer!=`string`||t.customer===``||t.customer.length>200)&&!(!We(t.issuedAt)||!We(t.updatesUntil))&&!(t.updatesUntil<t.issuedAt))return e}const qe=e=>new TextEncoder().encode(e),Je=e=>qe(`${Ne}.${e}`);async function Ye(e,t){let n=(e??``).trim();if(n===``||n.length>Pe)return;let r=n.split(`.`);if(r.length!==3)return;let[i,a,o]=r;if(i!==Ne)return;let s=ze(o);if(s===void 0||s.length!==64||!await crypto.subtle.verify(`Ed25519`,t,s,Je(a)).catch(()=>!1))return;let c=ze(a);if(c!==void 0)try{return Ke(JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(c)))}catch{return}}function Xe(e){let t=ze(typeof e.publicKey==`string`?e.publicKey.trim():``),n=t!==void 0&&t.length===32&&We(e.buildDate),r,i=async()=>{r??=crypto.subtle.importKey(`raw`,t,{name:`Ed25519`},!1,[`verify`]);try{return await r}catch{r=void 0;return}};return async({key:t,hostname:r})=>{let a=Ve(r);if(a===void 0)return Le;if(Ue(a))return{ok:!0,kind:`exempt`,hostname:a};if(!n)return Le;let o=await i();if(o===void 0)return Le;let s=await Ye(t,o);return s===void 0||!s.sites.includes(a)?Le:e.buildDate>s.updatesUntil?{ok:!1,error:`LICENCE_EXPIRED`,updatesUntil:s.updatesUntil}:{ok:!0,kind:`licensed`,hostname:a,licence:s}}}const Ze=`config:v1`;function Qe(e){return!!e.turnstileSiteKey&&!!e.turnstileSecretKey}let $e=null;function et(e){if(e==null||e===``)return 365;let t=Number(e);if(!Number.isFinite(t))return 365;let n=Math.round(t);return Math.max(30,Math.min(3650,n))}function tt(e){let t=e&&typeof e==`object`?e:{};return{eventCollection:re(t.eventCollection,`events`),checkoutReturnPath:ne(t.checkoutReturnPath,`/tickets/checkout`),orderPagePath:ne(t.orderPagePath,`/tickets/order`),retentionDays:et(t.retentionDays),defaultTimezone:u(t.defaultTimezone)??``,turnstileSiteKey:be(t.turnstileSiteKey),turnstileSecretKey:be(t.turnstileSecretKey),autoRefund:t.autoRefund!==!1,licenceKey:typeof t.licenceKey==`string`?t.licenceKey.trim().slice(0,Pe):``,licenceCode:ke(t.licenceCode)??``}}async function Y(e,t,n=Date.now()){return $e&&n-$e.at<6e4||($e={config:tt((await t.call(()=>e.getVersioned(Ze)))?.value),at:n}),$e.config}async function nt(e,t,n){for(let r=0;r<2;r++){let r=await t.call(()=>e.getVersioned(Ze)),i=tt({...tt(r?.value),...n});if((await t.call(()=>e.compareAndSet(Ze,r?.revision??null,i))).applied)return $e={config:i,at:Date.now()},i}throw Error(`Unable to save configuration.`)}const rt=2e3;function it(e){let t=e&&typeof e==`object`?e:{},n=typeof t.instructions==`string`?t.instructions.trim().slice(0,rt):``;if(!(t.enabled!==!0&&!n))return{enabled:t.enabled===!0&&n.length>0,instructions:n}}const X=Object.freeze({tiers:20,name:80,maxPerOrder:50,defaultMaxPerOrder:10,capacity:1e5}),at=new Set([`bif`,`clp`,`djf`,`gnf`,`jpy`,`kmf`,`krw`,`mga`,`pyg`,`rwf`,`ugx`,`vnd`,`vuv`,`xaf`,`xof`,`xpf`]);function ot(e){let t=typeof e==`string`?e.trim().toLowerCase():``;return/^[a-z]{3}$/.test(t)?t:``}function st(e){return at.has(String(e??``).trim().toLowerCase())?0:2}function ct(e,t){let n=e.trim().toLowerCase();if(n===`free`)return 0;let r=n.replace(/^[a-z]{3}\s*(?=\d)/,``);if(/^0+(\.0+)?$/.test(r))return 0;let i=st(t);if(!(i===0?/^\d{1,9}$/:/^\d{1,9}(\.\d{1,2})?$/).test(r))return null;let[a,o=``]=r.split(`.`),s=Number(a)*10**i+Number((o+`00`).slice(0,i)||0);return Number.isSafeInteger(s)?s:null}function lt(e,t){if(e===0)return`Free`;let n=st(t);try{return new Intl.NumberFormat(`en`,{style:`currency`,currency:t.toUpperCase()}).format(e/10**n).replace(/[  ]/g,` `)}catch{return`${(e/10**n).toFixed(n)} ${t.toUpperCase()}`}}function ut(e,t){if(e===0)return`free`;let n=st(t);return(e/10**n).toFixed(n)}function dt(e){return e.toLowerCase().normalize(`NFKD`).replace(/[̀-ͯ]/g,``).replace(/[^a-z0-9]+/g,`_`).replace(/^_+|_+$/g,``).slice(0,40)||`tier`}function ft(e,t){let n=e.trim();if(/^\d{4}-\d{2}-\d{2}$/.test(n)){let e=Date.parse(`${n}T00:00:00Z`);return Number.isNaN(e)?null:new Date(t?e+864e5:e).toISOString()}let r=Date.parse(n);return/^\d{4}-\d{2}-\d{2}T/.test(n)&&!Number.isNaN(r)?new Date(r).toISOString():null}function pt(e,t){if(!e)return``;let n=Date.parse(e),r=new Date(t?n-864e5:n).toISOString();return r.endsWith(`T00:00:00.000Z`)?r.slice(0,10):e}function mt(e,t){let n=[],r=[],i=new Set;return(e??``).split(/\r\n|\r|\n/).forEach((e,a)=>{let o=e.trim();if(!o||o.startsWith(`#`))return;let s=`Line ${a+1}`,[c=``,l=``,u=``,d=``,f=``,p=``]=o.split(`|`).map(e=>e.trim());if(!c)return void r.push(`${s}: start with the tier's name.`);if(c.length>X.name)return void r.push(`${s}: the name is longer than ${X.name} characters.`);if(!l)return void r.push(`${s}: add a price (for example 45.00, or "free").`);if(!t)return void r.push(`${s}: set the currency first.`);let m=ct(l,t);if(m===null){r.push(`${s}: "${l}" isn't a price${st(t)===0?` (${t.toUpperCase()} has no decimals)`:` (use 45 or 45.50)`}.`);return}let h=u?Number(u):0;if(!Number.isSafeInteger(h)||h<0||h>X.capacity)return void r.push(`${s}: capacity must be a whole number (0 = no tier limit).`);let g=d?Number(d):X.defaultMaxPerOrder;if(!Number.isSafeInteger(g)||g<1||g>X.maxPerOrder)return void r.push(`${s}: max per order must be 1 to ${X.maxPerOrder}.`);let _=f?ft(f,!1):null;if(f&&!_)return void r.push(`${s}: "${f}" isn't a date (use YYYY-MM-DD).`);let v=p?ft(p,!0):null;if(p&&!v)return void r.push(`${s}: "${p}" isn't a date (use YYYY-MM-DD).`);if(_&&v&&v<=_)return void r.push(`${s}: sales end before they start.`);let y=dt(c);for(let e=2;i.has(y);e++)y=`${dt(c)}_${e}`;i.add(y),n.push({id:y,name:c,priceMinor:m,capacity:h,maxPerOrder:g,..._?{salesStart:_}:{},...v?{salesEnd:v}:{}})}),n.length>X.tiers&&r.push(`At most ${X.tiers} tiers per event.`),{tiers:n.slice(0,X.tiers),errors:r}}function ht(e,t){let n=e&&typeof e==`object`?e:{},r=ot(n.currency),i=new Set,a=[];for(let e of Array.isArray(n.tiers)?n.tiers.slice(0,X.tiers):[]){let t=e??{},n=typeof t.id==`string`&&/^[a-z0-9_]{1,48}$/.test(t.id)&&!i.has(t.id)?t.id:null,r=typeof t.name==`string`?t.name.trim().slice(0,X.name):``,o=(e,t,n,r)=>Number.isSafeInteger(e)&&e>=t&&e<=n?e:r;if(!n||!r||!Number.isSafeInteger(t.priceMinor)||t.priceMinor<0)continue;i.add(n);let s=e=>typeof e==`string`&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():void 0,c=s(t.salesStart),l=s(t.salesEnd);a.push({id:n,name:r,priceMinor:t.priceMinor,capacity:o(t.capacity,0,X.capacity,0),maxPerOrder:o(t.maxPerOrder,1,X.maxPerOrder,X.defaultMaxPerOrder),...c?{salesStart:c}:{},...l?{salesEnd:l}:{}})}let o=Number.isSafeInteger(n.capacity)&&n.capacity>=0?n.capacity:void 0,s=pe(n.questions),c=it(n.invoice);return{poolKey:t,currency:r,tiers:a,...o===void 0?{}:{capacity:o},...s.length?{questions:s}:{},...c?{invoice:c}:{},...n.cardOff===!0?{cardOff:!0}:{},...Number.isSafeInteger(n.selfCancelHours)&&n.selfCancelHours>=0&&n.selfCancelHours<=8760?{selfCancelHours:n.selfCancelHours}:{}}}function gt(e,t,n){return e.salesStart&&n<Date.parse(e.salesStart)?`not_yet`:e.salesEnd&&n>=Date.parse(e.salesEnd)?`ended`:t<=0?`sold_out`:`on_sale`}function _t(e,t){if(e.capacity!==void 0)return e.capacity;let n=Number(t.data?.capacity??0);return Number.isSafeInteger(n)&&n>0?n:0}function vt(e,t,n,r=0){let i=(e?.taken??0)+(e?.held??0),a=t===0?1/0:Math.max(0,t-i);if(!n||r===0)return a;let o=e?.buckets?.[n];return Math.min(a,Math.max(0,r-(o?.taken??0)-(o?.held??0)))}const yt=e=>Number.isFinite(e)?e:null;async function bt(e,t,n,r,i=Date.now()){if(!n)return{ok:!1,error:`MISSING_EVENT`};if(r&&Number.isNaN(Date.parse(r)))return{ok:!1,error:`INVALID_OCCURRENCE`};let a=await Y(e.kv,t,i),{eventCollection:o}=a,s=await t.call(()=>e.content.get(o,n));if(!s||s.status!==`published`)return{ok:!1,error:`EVENT_NOT_FOUND`};if(!ue(s))return{ok:!1,error:`EVENT_NOT_TICKETED`};let c=de(s),u=ht(await t.call(()=>e.storage.tier_settings.get(c)),c),d=_t(u,s),f=await t.call(()=>e.kv.getVersioned(m(c,r))),p=f?_(f.value):null,h=u.tiers.map(e=>{let t=vt(p,d,e.id,e.capacity);return{id:e.id,name:e.name,priceMinor:e.priceMinor,price:lt(e.priceMinor,u.currency),seatsLeft:yt(t),maxPerOrder:Math.max(0,Math.min(e.maxPerOrder,t)),status:gt(e,t,i),...e.salesStart?{salesStart:e.salesStart}:{},...e.salesEnd?{salesEnd:e.salesEnd}:{}}}),g=l(s.data??{},r);return{ok:!0,eventId:n,occurrenceStart:r,...g?.startsAt?{startsAt:g.startsAt}:{},...g?{allDay:g.allDay}:{},currency:u.currency,open:u.tiers.length>0&&u.currency!==``,seatsLeft:yt(vt(p,d)),tiers:h,questions:u.questions??[],invoice:u.invoice?.enabled===!0,card:u.cardOff!==!0,...Qe(a)?{turnstileSiteKey:a.turnstileSiteKey}:{}}}const xt=new Map;function St(e){let t=0,n=(n,r)=>{let a=[],o=``,s=()=>{o&&a.push(o),o=``};for(;t<e.length;){let c=e[t];if(c===`{`)s(),t++,a.push(i(n));else if(c===`}`&&r)break;else c===`#`&&n?(s(),a.push(0),t++):(o+=c,t++)}return s(),a},r=()=>{for(;e[t]===` `;)t++;let n=t;for(;t<e.length&&!/[\s,{}]/.test(e[t]);)t++;let r=e.slice(n,t);for(;e[t]===` `||e[t]===`
1
+ function e(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}}function t(e){return e}var n=class extends Error{code=`RPC_BUDGET_EXCEEDED`;constructor(){super(`RPC budget exhausted for this invocation.`)}},r=class{limit;used=0;constructor(e=10){this.limit=e}get remaining(){return this.limit-this.used}has(e){return this.remaining>=e}async call(e){if(this.used>=this.limit)throw new n;return this.used++,e()}},i=class{parent;reserve;constructor(e,t){this.parent=e,this.reserve=t}get remaining(){return this.parent.remaining-this.reserve}has(e){return this.remaining>=e}call(e){return this.remaining<=0?Promise.reject(new n):this.parent.call(e)}};function a(e,t,n,r,i){let a=t?.[n];typeof a!=`function`||!e.has(1)||e.call(async()=>{await a.call(t,r,i)}).catch(()=>{})}function o(e){return typeof e==`string`?e.trim():``}function s(e){return/^\d{4}-\d{2}-\d{2}$/.test(e)&&!Number.isNaN(Date.parse(`${e}T00:00:00Z`))}function c(e,t){let n=new Date(`${e}T00:00:00Z`);return n.setUTCDate(n.getUTCDate()+t),n.toISOString().slice(0,10)}function l(e,t=``){let n=o(e.timezone)||void 0;if(e.all_day===!0){let r=o(e.start_date)||o(e.start).slice(0,10),i=t?t.slice(0,10):r;if(!s(i))return null;let a=o(e.end_date),l=s(r)&&s(a)?Math.max(0,Math.round((Date.parse(a)-Date.parse(r))/864e5)):0;return{startsAt:`${i}T00:00:00.000Z`,allDay:!0,startDate:i,end:c(i,l),timezone:n}}let r=o(e.start)||o(e.start_date);if(!o(e.start)&&s(r))return l({...e,all_day:!0},t);let i=Date.parse(r),a=t?Date.parse(t):i;if(Number.isNaN(a))return null;let u=Date.parse(o(e.end)||o(e.end_date)),d=!Number.isNaN(i)&&!Number.isNaN(u)&&u>i?u-i:36e5;return{startsAt:new Date(a).toISOString(),allDay:!1,end:new Date(a+d).toISOString(),timezone:n}}function u(e){if(typeof e!=`string`)return;let t=e.trim();if(!(!t||t.length>100))try{return new Intl.DateTimeFormat(`en`,{timeZone:t}),t}catch{return}}function d(e,t,n){let r=String(e??``).trim();if(!r)return``;let i=new Date(r);if(Number.isNaN(i.getTime()))return r;let a=u(t),o=a??`UTC`,s=String(n??``).trim()||`en`,c=new Intl.DateTimeFormat(s,{timeZone:o,dateStyle:`medium`,timeStyle:`short`}).format(i);if(!a)return`${c} UTC`;let l=new Intl.DateTimeFormat(s,{timeZone:a,timeZoneName:`short`}).formatToParts(i).find(e=>e.type===`timeZoneName`)?.value;return l?`${c} ${l}`:`${c} UTC`}const f=/^[a-z0-9_]{1,48}$/,p=`ledger:`;function m(e,t=``){let n=e.trim(),r=t.trim();return r?`${p}${n}:${r}`:`${p}${n}`}function h(e,t){let n=e.slice(7);return t&&n.endsWith(`:${t}`)?n.slice(0,-(t.length+1)):n}function g(e,t=``,n=``){return{capacity:e,poolKey:n,taken:0,held:0,waitlistCount:0,nextPosition:1,occurrenceStart:t,updatedAt:new Date().toISOString()}}function _(e,t=0){if(!e||typeof e!=`object`)return null;let n=e=>typeof e==`number`&&Number.isFinite(e)&&e>=0?Math.floor(e):0,r=u(e.timezone);return{capacity:typeof e.capacity==`number`?n(e.capacity):t,taken:n(e.taken),held:n(e.held),waitlistCount:n(e.waitlistCount),nextPosition:Math.max(1,n(e.nextPosition)),poolKey:typeof e.poolKey==`string`?e.poolKey:``,occurrenceStart:typeof e.occurrenceStart==`string`?e.occurrenceStart:``,...typeof e.label==`string`&&e.label?{label:e.label.slice(0,200)}:{},...r?{timezone:r}:{},...v(e.buckets,n),updatedAt:typeof e.updatedAt==`string`?e.updatedAt:new Date().toISOString()}}function v(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return{};let n={};for(let[r,i]of Object.entries(e).slice(0,100)){if(!f.test(r)||!i||typeof i!=`object`)continue;let e=i;n[r]={capacity:t(e.capacity),taken:t(e.taken),held:t(e.held)}}return Object.keys(n).length?{buckets:n}:{}}function y(e,t,n){let r=e.buckets?.[t],i=n??r?.capacity??0;return i===0?1/0:Math.max(0,i-(r?.taken??0)-(r?.held??0))}function b(e){return e.capacity===0?1/0:Math.max(0,e.capacity-e.taken-e.held)}async function x(e,t,n,r,i=2){for(let a=0;a<i&&t.has(2);a++){let i=await t.call(()=>e.getVersioned(n)),a=r(i?_(i.value):null);if(`abort`in a)return{applied:!1,result:a.abort,reason:`ABORTED`};let o={...a.next,updatedAt:new Date().toISOString()};if((await t.call(()=>e.compareAndSet(n,i?.revision??null,o))).applied)return{applied:!0,result:a.result,ledger:o}}return{applied:!1,reason:`CONFLICT`}}function S(e,t){if(!t)return e;let n={...e??{}};for(let[e,r]of Object.entries(t)){let t=n[e];if(!t)continue;let i=Math.min(r.heldToTaken??0,t.held);n[e]={capacity:t.capacity,taken:Math.max(0,t.taken-(r.taken??0))+i,held:Math.max(0,t.held-(r.held??0)-i)}}return n}async function C(e,t,n,r,i=2){let a=await x(e,t,n,e=>{if(!e)return{abort:!0};let t=Math.min(r.heldToTaken??0,e.held);return{next:{...e,taken:Math.max(0,e.taken-(r.taken??0))+t,held:Math.max(0,e.held-(r.held??0)-t),waitlistCount:Math.max(0,e.waitlistCount-(r.waitlistCount??0)),...e.buckets||r.buckets?{buckets:S(e.buckets,r.buckets)}:{}},result:!0}},i);return a.applied||a.reason===`ABORTED`}const w=/^[a-z0-9_]{1,48}$/;async function T(e,t,n,r){let{capacity:i,lines:a,poolKey:o=``,occurrenceStart:s=``,label:c,timezone:l,confirm:d=!1,maxAttempts:f=2}=r,p=u(l),m=new Set;for(let e of a){if(!(w.test(e.bucket)&&!m.has(e.bucket)&&Number.isSafeInteger(e.quantity)&&e.quantity>=1&&Number.isSafeInteger(e.bucketCapacity)&&e.bucketCapacity>=0))return{ok:!1,reason:`INVALID`,bucket:e.bucket};m.add(e.bucket)}if(a.length===0||!Number.isSafeInteger(i)||i<0)return{ok:!1,reason:`INVALID`};let h=a.reduce((e,t)=>e+t.quantity,0),_=await x(e,t,n,e=>{let t={...e??g(i,s,o),capacity:i,poolKey:e?.poolKey||o,...c?{label:c.slice(0,200)}:{},...p?{timezone:p}:{}};if(b(t)<h)return{abort:{ok:!1,reason:`FULL`}};for(let e of a)if(y(t,e.bucket,e.bucketCapacity)<e.quantity)return{abort:{ok:!1,reason:`BUCKET_FULL`,bucket:e.bucket}};let n={...t.buckets??{}};for(let e of a){let t=n[e.bucket]??{capacity:e.bucketCapacity,taken:0,held:0};n[e.bucket]={capacity:e.bucketCapacity,taken:t.taken+(d?e.quantity:0),held:t.held+(d?0:e.quantity)}}return{next:{...t,taken:t.taken+(d?h:0),held:t.held+(d?0:h),buckets:n},result:{ok:!0}}},f);return _.applied?{ok:!0,ledger:_.ledger}:_.reason===`ABORTED`&&_.result&&!_.result.ok?{ok:!1,reason:_.result.reason,..._.result.bucket?{bucket:_.result.bucket}:{}}:{ok:!1,reason:`BUSY`}}async function E(e,t,n,r=2){let i;do{if(!e.has(r))return`TOO_LARGE`;let a=await e.call(()=>t(i));for(let{data:e}of a.items)if(n(e)===!1)return`BUSY`;i=a.hasMore?a.cursor:void 0}while(i);return`OK`}function D(e,t){let n=new Set([...Object.keys(e??{}),...Object.keys(t??{})]);for(let r of n){let n=e?.[r],i=t?.[r];if((n?.taken??0)!==(i?.taken??0)||(n?.held??0)!==(i?.held??0))return!1}return!0}async function O(e,t,n,r,o={}){let s=o.nowMs??Date.now(),c=o.quietMs??6e5,l=e=>{let t=e?Date.parse(e):NaN;return!Number.isFinite(t)||s-t>=c},u=await t.call(()=>e.getVersioned(n)),d=u?_(u.value):null;if(!u||!d)return{status:`MISSING`,key:n};if(!l(d.updatedAt))return{status:`BUSY`,key:n};let f=d.poolKey||h(n,d.occurrenceStart),p=await r({ledger:d,poolKey:f,occurrenceStart:d.occurrenceStart,budget:new i(t,1),isQuiet:l});if(p===`BUSY`||p===`TOO_LARGE`)return{status:p,key:n};let m;if(d.buckets||p.buckets){m={};for(let e of new Set([...Object.keys(d.buckets??{}),...Object.keys(p.buckets??{})]))m[e]={capacity:d.buckets?.[e]?.capacity??0,taken:p.buckets?.[e]?.taken??0,held:p.buckets?.[e]?.held??0}}let g={...d,poolKey:f,taken:p.taken,held:p.held,waitlistCount:p.waitlistCount??d.waitlistCount,nextPosition:Math.max(d.nextPosition,(p.maxPosition??0)+1),...m?{buckets:m}:{}};return g.taken===d.taken&&g.held===d.held&&g.waitlistCount===d.waitlistCount&&g.nextPosition===d.nextPosition&&g.poolKey===d.poolKey&&D(g.buckets,d.buckets)?{status:`UNCHANGED`,key:n}:(await t.call(()=>e.compareAndSet(n,u.revision,{...g,updatedAt:new Date(s).toISOString()}))).applied?(a(t,o.log,`warn`,`Seat ledger corrected from records`,{key:n,before:d,after:g}),{status:`CORRECTED`,key:n,before:d,after:g}):{status:`CONFLICT`,key:n}}async function k(e,t,n,r){let a=(await t.call(()=>e.list(p))).map(e=>e.key).sort();if(a.length===0)return null;let o=await t.call(()=>e.getVersioned(n)),s=a.find(e=>!o||e>o.value)??a[0],c=await r(s,new i(t,1));return await t.call(()=>e.compareAndSet(n,o?.revision??null,s)),c}const A=`state:schedules`;let j=null;function M(e){return e.map(e=>`${e.name}=${e.schedule}`).sort().join(`;`)}function N(e){return typeof e==`object`&&!!e&&e.v===1&&typeof e.fingerprint==`string`&&Array.isArray(e.done)}async function P(e,t,n,r){let i=M(r);if(j===i)return`OK`;if(!t||!n.has(1))return`SKIPPED`;let a=await n.call(()=>e.getVersioned(A));if(a?.value===i)return j=i,`OK`;let o=new Set(N(a?.value)&&a.value.fingerprint===i?a.value.done:[]),s=r.filter(e=>!o.has(`${e.name}=${e.schedule}`)),c=n.remaining-1;if(c<1)return`SKIPPED`;let l=s.slice(0,c);for(let e of l)await n.call(()=>t.schedule(e.name,{schedule:e.schedule})),o.add(`${e.name}=${e.schedule}`);let u=r.every(e=>o.has(`${e.name}=${e.schedule}`)),d=u?i:{v:1,fingerprint:i,done:[...o]};return await n.call(()=>e.compareAndSet(A,a?.revision??null,d)),u?(j=i,`SCHEDULED`):`PARTIAL`}function F(e){return Array.from(new Uint8Array(e),e=>e.toString(16).padStart(2,`0`)).join(``)}async function I(e,t,n){if(!e||e.length<32)throw Error(`HMAC secret is missing or too short.`);if(!/^[a-z][a-z0-9-]{0,31}$/.test(t))throw Error(`Token purpose must be a short lowercase word.`);let r=await crypto.subtle.importKey(`raw`,new TextEncoder().encode(e),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]),i=`${t}:${n.join(`:`)}`;return F(await crypto.subtle.sign(`HMAC`,r,new TextEncoder().encode(i)))}async function L(e){return F(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e)))}function ee(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}function R(e){let t=new Uint8Array(e);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,`0`)).join(``)}function z(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}async function B(e,t,n=`state:signing-key`){for(let r=0;r<2;r++){let r=await t.call(()=>e.getVersioned(n));if(r&&z(r.value))return r.value;let i=R(32);if((await t.call(()=>e.compareAndSet(n,r?.revision??null,i))).applied)return i}throw Error(`Unable to initialise the signing secret.`)}function te(e){return/ENCRYPTION_KEY|PluginSettingEncryption/i.test(String(e))}function ne(e,t){let n=typeof e==`string`?e.trim():``;return!n.startsWith(`/`)||n.startsWith(`//`)||/[\s#?\\]/.test(n)?t:n.slice(0,200)}function re(e,t=`events`){let n=typeof e==`string`?e.trim():``;return/^[a-z][a-z0-9_]{0,63}$/.test(n)?n:t}const ie=/^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,}$/;function ae(e){return typeof e==`string`&&e.length<=320&&ie.test(e)}function oe(e){let t=typeof e==`string`?e.trim().toLowerCase():``;return ae(t)?t:``}function se(e){return typeof e==`string`&&/^[A-Za-z0-9_-]{16,64}$/.test(e)}function ce(e){return!e||e.trim().length===0}function le(e,t=Date.now()){if(!e||typeof e!=`number`)return!0;let n=t-e;return n>=1500&&n<=72e5}function V(e){let t=e.data?.inventory_mode;return t===`rsvp`||t===`tickets`?t:null}function ue(e){return V(e)===`tickets`}function de(e){return(e.translationGroup||e.id).trim()}Object.freeze([{slug:`inventory_mode`,label:`Seats managed by`,type:`select`,translatable:!1,validation:{options:[`rsvp`,`tickets`]}},{slug:`capacity`,label:`Capacity per date (0 = unlimited)`,type:`integer`,translatable:!1,defaultValue:0},{slug:`allow_waitlist`,label:`Allow waitlist when full`,type:`boolean`,translatable:!1,defaultValue:!1}]);const H=[`text`,`textarea`,`email`,`phone`,`number`,`date`,`checkbox`,`select`],U=Object.freeze({questions:20,label:400,options:30,option:80,text:500,textarea:2e3});function W(e){return e.toLowerCase().normalize(`NFKD`).replace(/[̀-ͯ]/g,``).replace(/[^a-z0-9]+/g,`_`).replace(/^_+|_+$/g,``).slice(0,40)||`question`}function G(e,t){let n=e;for(let r=2;t.has(n);r++)n=`${e}_${r}`;return t.add(n),n}function fe(e){let t=[],n=[],r=[],i=new Set;return(e??``).split(/\r\n|\r|\n/).forEach((e,a)=>{let o=e.trim();if(!o||o.startsWith(`#`))return;let s=a+1,c=`Line ${s}`,[l=``,u=``,d=``,f=``]=o.split(`|`).map(e=>e.trim());if(!l){n.push(`${c}: start with the question's label.`),r.push({line:s,code:`NO_LABEL`});return}if(l.length>U.label){n.push(`${c}: the label is longer than ${U.label} characters.`),r.push({line:s,code:`LABEL_TOO_LONG`,limit:U.label});return}let p=u.toLowerCase()||`text`;if(!H.includes(p)){n.push(`${c}: "${u}" isn't a type. Use one of: ${H.join(`, `)}.`),r.push({line:s,code:`BAD_TYPE`,value:u});return}let m=d.toLowerCase();if(m&&m!==`required`&&m!==`optional`){n.push(`${c}: write "required" or "optional" in the third column.`),r.push({line:s,code:`BAD_REQUIRED`});return}let h={id:G(W(l),i),label:l,type:p,required:m===`required`};if(p===`select`){let e=[...new Set(f.split(`,`).map(e=>e.trim()).filter(Boolean))];if(e.length===0){n.push(`${c}: a select question needs options, for example "S, M, L".`),r.push({line:s,code:`NO_OPTIONS`});return}if(e.length>U.options){n.push(`${c}: at most ${U.options} options.`),r.push({line:s,code:`TOO_MANY_OPTIONS`,limit:U.options});return}if(e.some(e=>e.length>U.option)){n.push(`${c}: options can be at most ${U.option} characters.`),r.push({line:s,code:`OPTION_TOO_LONG`,limit:U.option});return}h.options=e}else if(f){n.push(`${c}: only select questions take options.`),r.push({line:s,code:`OPTIONS_NOT_ALLOWED`});return}t.push(h)}),t.length>U.questions&&(n.push(`At most ${U.questions} questions per event.`),r.push({line:null,code:`TOO_MANY_QUESTIONS`,limit:U.questions})),{questions:t.slice(0,U.questions),errors:n,issues:r}}function pe(e){if(!Array.isArray(e))return[];let t=new Set,n=[];for(let r of e.slice(0,U.questions)){if(!r||typeof r!=`object`)continue;let e=r,i=typeof e.label==`string`?e.label.trim().slice(0,U.label):``,a=H.includes(e.type)?e.type:null;if(!i||!a)continue;let o=typeof e.id==`string`&&/^[a-z0-9_]{1,48}$/.test(e.id)&&!t.has(e.id)?(t.add(e.id),e.id):G(W(i),t),s=a===`select`&&Array.isArray(e.options)?e.options.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>e.trim().slice(0,U.option)).slice(0,U.options):void 0;a===`select`&&!s?.length||n.push({id:o,label:i,type:a,required:e.required===!0,...s?{options:s}:{}})}return n}const K=/^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,}$/,me=/^\+?[0-9 ()./-]{5,30}$/;function he(e,t){let n=t&&typeof t==`object`&&!Array.isArray(t)?t:{},r=[],i={};for(let t of e){let e=n[t.id];if(t.type===`checkbox`){let n=e===!0||e===`true`||e===`on`;t.required&&!n?i[t.id]=`REQUIRED`:r.push({id:t.id,label:t.label,value:n});continue}let a=typeof e==`string`?e.trim():typeof e==`number`&&Number.isFinite(e)?String(e):``;if(!a){t.required&&(i[t.id]=`REQUIRED`);continue}let o=t.type===`textarea`?U.textarea:U.text;if(a.length>o){i[t.id]=`TOO_LONG`;continue}let s=a,c=!0;switch(t.type){case`email`:c=K.test(a),s=a.toLowerCase();break;case`phone`:c=me.test(a);break;case`number`:s=Number(a),c=Number.isFinite(s);break;case`date`:c=/^\d{4}-\d{2}-\d{2}$/.test(a)&&!Number.isNaN(Date.parse(`${a}T00:00:00Z`));break;case`select`:c=t.options?.includes(a)??!1;break;case`text`:c=!/[\r\n]/.test(a);break;default:break}c?r.push({id:t.id,label:t.label,value:s}):i[t.id]=`INVALID`}return Object.keys(i).length?{ok:!1,errors:i}:{ok:!0,answers:r}}function ge(e){return(e??[]).map(e=>`${e.label}: ${typeof e.value==`boolean`?e.value?`Yes`:`No`:e.value}`).join(`; `)}const _e=`challenges.cloudflare.com`,ve=`https://${_e}/turnstile/v0/siteverify`;`${_e}`;const ye=/^[A-Za-z0-9_-]{10,100}$/;function be(e){let t=typeof e==`string`?e.trim():``;return ye.test(t)?t:``}function xe(e){return typeof e==`string`&&e.length>0&&e.length<=2048&&!/\s/.test(e)}async function Se(e,t,n){if(!xe(n.token))return{ok:!1,reason:`missing-input-response`};let r=new URLSearchParams({secret:n.secret,response:n.token});n.remoteIp&&r.set(`remoteip`,n.remoteIp);let i;try{i=await t.call(()=>e(ve,{method:`POST`,headers:{"content-type":`application/x-www-form-urlencoded`},body:r.toString()}))}catch(e){if(e?.code===`RPC_BUDGET_EXCEEDED`)throw e;return{ok:!0,verified:!1,reason:`unreachable`}}if(i.status>=500)return{ok:!0,verified:!1,reason:`http-${i.status}`};let a=await i.json().catch(()=>null);if(a?.success===!0)return{ok:!0,verified:!0};let o=Array.isArray(a?.[`error-codes`])?a[`error-codes`].map(String):[];return o.includes(`internal-error`)?{ok:!0,verified:!1,reason:`internal-error`}:{ok:!1,reason:o[0]??(a?`rejected`:`bad-response`)}}const q=`eventual-tickets`,Ce=`stripeSecretKey`,we=`stripeWebhookSecret`,Te=[`checkout.session.completed`,`checkout.session.expired`,`checkout.session.async_payment_succeeded`,`checkout.session.async_payment_failed`,`charge.refunded`];function Ee(e){if(typeof e!=`string`||!e.trim())return`missing`;let t=e.trim();return t.startsWith(`sk_test_`)?`test`:t.startsWith(`sk_live_`)?`live`:t.startsWith(`rk_test_`)?`restricted-test`:t.startsWith(`rk_live_`)?`restricted-live`:`unrecognised`}function De(e){return e===`live`||e===`restricted-live`}function Oe(e,t=q){let n=String(e??``).replace(/\/+$/,``);return/^https?:\/\//.test(n)?`${n}/_emdash/api/plugins/${t||q}/stripe/webhook`:null}async function ke(e,t,n){let r=String(e.site?.url??``).replace(/\/+$/,``),i=Oe(r,n||e.plugin?.id||q);try{let n=await t.call(()=>e.settings.get(Ce)),a=await t.call(()=>e.settings.get(we));!n&&!a&&await t.call(()=>e.settings.compareAndSet(we,`eventual-tickets:encryption-probe`,`probe`));let o=Ee(n),s=typeof a==`string`&&a.trim().startsWith(`whsec_`);return{encryption:!0,stripeKey:o,webhookSecret:s,siteUrl:r,webhookUrl:i,ready:o!==`missing`&&o!==`unrecognised`&&s&&i!==null}}catch(e){if(!te(e))throw e;return{encryption:!1,stripeKey:`missing`,webhookSecret:!1,siteUrl:r,webhookUrl:i,ready:!1}}}function Ae(e){if(typeof e!=`string`)return;let t=e.toUpperCase().replace(/[\s-]+/g,``);if(t.startsWith(`EVT`)&&(t=t.slice(3).replace(/O/g,`0`).replace(/[IL]/g,`1`),!(t.length!==16||![...t].every(e=>`0123456789ABCDEFGHJKMNPQRSTVWXYZ`.includes(e)))))return`EVT-${t.slice(0,4)}-${t.slice(4,8)}-${t.slice(8,12)}-${t.slice(12,16)}`}function je(e){let t=e;if(!(!t||typeof t!=`object`||typeof t.licence!=`string`||typeof t.seats!=`number`))return{licence:t.licence,seats:t.seats,used:typeof t.used==`number`?t.used:0,hostnames:Array.isArray(t.hostnames)?t.hostnames.filter(e=>typeof e==`string`).slice(0,100):[],updatesUntil:typeof t.updatesUntil==`string`?t.updatesUntil:``,movesLeft:typeof t.movesLeft==`number`?t.movesLeft:null}}const Me=[`CODE_INVALID`,`CODE_REVOKED`,`HOSTNAME_INVALID`,`NO_FREE_SITE`,`NOT_ACTIVE`,`MOVES_EXHAUSTED`,`TOO_MANY_REQUESTS`];async function Ne(e,t,n,r,i){if(!e)return{ok:!1,error:`UNREACHABLE`};let a;try{a=await t.call(()=>e(`https://licences.eventual.rsvp/v1/${n}`,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`},body:JSON.stringify({code:r,hostname:i})}))}catch{return{ok:!1,error:`UNREACHABLE`}}if(a.status===429)return{ok:!1,error:`TOO_MANY_REQUESTS`};let o;try{o=await a.json()}catch{return{ok:!1,error:`UNREACHABLE`}}let s=je(o?.status);if(o?.ok===!0){let e=typeof o.key==`string`&&o.key.startsWith(`evt1.`)?o.key:void 0;return n===`activate`&&!e&&o.exempt!==!0?{ok:!1,error:`UNREACHABLE`}:{ok:!0,...e?{key:e}:{},...o.exempt===!0?{exempt:!0}:{},...s?{status:s}:{}}}return{ok:!1,error:Me.includes(o?.error)?o.error:`UNREACHABLE`,...s?{status:s}:{}}}const Pe=`evt1`,Fe=4096,Ie=new Set([`localhost`,`127.0.0.1`,`[::1]`]),Le=[`localhost`,`workers.dev`,`pages.dev`],Re={ok:!1,error:`LICENCE_INVALID`},ze=/^[A-Za-z0-9_-]*$/;function Be(e){if(typeof e!=`string`||e===``||!ze.test(e))return;let t=e.length%4;if(t===1)return;let n=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`)+(t===0?``:`=`.repeat(4-t)),r;try{r=atob(n)}catch{return}return Uint8Array.from(r,e=>e.charCodeAt(0))}const Ve=/^[A-Za-z0-9.\-:[\]]+$/;function He(e){if(typeof e!=`string`||!Ve.test(e))return;let t;try{t=new URL(`http://${e}/`)}catch{return}let n=t.hostname.endsWith(`.`)?t.hostname.slice(0,-1):t.hostname;return n===``?void 0:n}function Ue(e){let t=typeof e==`string`?e.trim():``;if(t===``||!URL.canParse(t))return``;let n=new URL(t);return n.protocol!==`https:`&&n.protocol!==`http:`?``:He(n.hostname)??``}function We(e){let t=He(e);return t===void 0?!1:Ie.has(t)?!0:Le.some(e=>t.endsWith(`.${e}`)&&t.length>e.length+1)}function Ge(e){if(typeof e!=`string`)return!1;let t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e);if(!t)return!1;let[n,r,i]=[Number(t[1]),Number(t[2]),Number(t[3])],a=new Date(Date.UTC(n,r-1,i));return a.getUTCFullYear()===n&&a.getUTCMonth()===r-1&&a.getUTCDate()===i}function Ke(e){return typeof e==`string`&&He(e)===e}function qe(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(t.v!==1||t.product!==`tickets`)&&!(!Array.isArray(t.sites)||t.sites.length===0||t.sites.length>100)&&t.sites.every(Ke)&&!(typeof t.customer!=`string`||t.customer===``||t.customer.length>200)&&!(!Ge(t.issuedAt)||!Ge(t.updatesUntil))&&!(t.updatesUntil<t.issuedAt))return e}const Je=e=>new TextEncoder().encode(e),Ye=e=>Je(`${Pe}.${e}`);async function Xe(e,t){let n=(e??``).trim();if(n===``||n.length>Fe)return;let r=n.split(`.`);if(r.length!==3)return;let[i,a,o]=r;if(i!==Pe)return;let s=Be(o);if(s===void 0||s.length!==64||!await crypto.subtle.verify(`Ed25519`,t,s,Ye(a)).catch(()=>!1))return;let c=Be(a);if(c!==void 0)try{return qe(JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(c)))}catch{return}}function Ze(e){let t=Be(typeof e.publicKey==`string`?e.publicKey.trim():``),n=t!==void 0&&t.length===32&&Ge(e.buildDate),r,i=async()=>{r??=crypto.subtle.importKey(`raw`,t,{name:`Ed25519`},!1,[`verify`]);try{return await r}catch{r=void 0;return}};return async({key:t,hostname:r})=>{let a=He(r);if(a===void 0)return Re;if(We(a))return{ok:!0,kind:`exempt`,hostname:a};if(!n)return Re;let o=await i();if(o===void 0)return Re;let s=await Xe(t,o);return s===void 0||!s.sites.includes(a)?Re:e.buildDate>s.updatesUntil?{ok:!1,error:`LICENCE_EXPIRED`,updatesUntil:s.updatesUntil}:{ok:!0,kind:`licensed`,hostname:a,licence:s}}}const Qe=`config:v1`;function $e(e){return!!e.turnstileSiteKey&&!!e.turnstileSecretKey}let et=null;function tt(e){if(e==null||e===``)return 365;let t=Number(e);if(!Number.isFinite(t))return 365;let n=Math.round(t);return Math.max(30,Math.min(3650,n))}function nt(e){let t=e&&typeof e==`object`?e:{};return{eventCollection:re(t.eventCollection,`events`),checkoutReturnPath:ne(t.checkoutReturnPath,`/tickets/checkout`),orderPagePath:ne(t.orderPagePath,`/tickets/order`),retentionDays:tt(t.retentionDays),defaultTimezone:u(t.defaultTimezone)??``,turnstileSiteKey:be(t.turnstileSiteKey),turnstileSecretKey:be(t.turnstileSecretKey),autoRefund:t.autoRefund!==!1,licenceKey:typeof t.licenceKey==`string`?t.licenceKey.trim().slice(0,Fe):``,licenceCode:Ae(t.licenceCode)??``}}async function J(e,t,n=Date.now()){return et&&n-et.at<6e4||(et={config:nt((await t.call(()=>e.getVersioned(Qe)))?.value),at:n}),et.config}async function Y(e,t,n){for(let r=0;r<2;r++){let r=await t.call(()=>e.getVersioned(Qe)),i=nt({...nt(r?.value),...n});if((await t.call(()=>e.compareAndSet(Qe,r?.revision??null,i))).applied)return et={config:i,at:Date.now()},i}throw Error(`Unable to save configuration.`)}const rt=2e3;function it(e){let t=e&&typeof e==`object`?e:{},n=typeof t.instructions==`string`?t.instructions.trim().slice(0,rt):``;if(!(t.enabled!==!0&&!n))return{enabled:t.enabled===!0&&n.length>0,instructions:n}}const X=Object.freeze({tiers:20,name:80,maxPerOrder:50,defaultMaxPerOrder:10,capacity:1e5}),at=new Set([`bif`,`clp`,`djf`,`gnf`,`jpy`,`kmf`,`krw`,`mga`,`pyg`,`rwf`,`ugx`,`vnd`,`vuv`,`xaf`,`xof`,`xpf`]);function ot(e){let t=typeof e==`string`?e.trim().toLowerCase():``;return/^[a-z]{3}$/.test(t)?t:``}function st(e){return at.has(String(e??``).trim().toLowerCase())?0:2}function ct(e,t){let n=e.trim().toLowerCase();if(n===`free`)return 0;let r=n.replace(/^[a-z]{3}\s*(?=\d)/,``);if(/^0+(\.0+)?$/.test(r))return 0;let i=st(t);if(!(i===0?/^\d{1,9}$/:/^\d{1,9}(\.\d{1,2})?$/).test(r))return null;let[a,o=``]=r.split(`.`),s=Number(a)*10**i+Number((o+`00`).slice(0,i)||0);return Number.isSafeInteger(s)?s:null}function lt(e,t){if(e===0)return`Free`;let n=st(t);try{return new Intl.NumberFormat(`en`,{style:`currency`,currency:t.toUpperCase()}).format(e/10**n).replace(/[  ]/g,` `)}catch{return`${(e/10**n).toFixed(n)} ${t.toUpperCase()}`}}function ut(e,t){if(e===0)return`free`;let n=st(t);return(e/10**n).toFixed(n)}function dt(e){return e.toLowerCase().normalize(`NFKD`).replace(/[̀-ͯ]/g,``).replace(/[^a-z0-9]+/g,`_`).replace(/^_+|_+$/g,``).slice(0,40)||`tier`}function ft(e,t){let n=e.trim();if(/^\d{4}-\d{2}-\d{2}$/.test(n)){let e=Date.parse(`${n}T00:00:00Z`);return Number.isNaN(e)?null:new Date(t?e+864e5:e).toISOString()}let r=Date.parse(n);return/^\d{4}-\d{2}-\d{2}T/.test(n)&&!Number.isNaN(r)?new Date(r).toISOString():null}function pt(e,t){if(!e)return``;let n=Date.parse(e),r=new Date(t?n-864e5:n).toISOString();return r.endsWith(`T00:00:00.000Z`)?r.slice(0,10):e}function mt(e,t){let n=[],r=[],i=new Set;return(e??``).split(/\r\n|\r|\n/).forEach((e,a)=>{let o=e.trim();if(!o||o.startsWith(`#`))return;let s=`Line ${a+1}`,[c=``,l=``,u=``,d=``,f=``,p=``]=o.split(`|`).map(e=>e.trim());if(!c)return void r.push(`${s}: start with the tier's name.`);if(c.length>X.name)return void r.push(`${s}: the name is longer than ${X.name} characters.`);if(!l)return void r.push(`${s}: add a price (for example 45.00, or "free").`);if(!t)return void r.push(`${s}: set the currency first.`);let m=ct(l,t);if(m===null){r.push(`${s}: "${l}" isn't a price${st(t)===0?` (${t.toUpperCase()} has no decimals)`:` (use 45 or 45.50)`}.`);return}let h=u?Number(u):0;if(!Number.isSafeInteger(h)||h<0||h>X.capacity)return void r.push(`${s}: capacity must be a whole number (0 = no tier limit).`);let g=d?Number(d):X.defaultMaxPerOrder;if(!Number.isSafeInteger(g)||g<1||g>X.maxPerOrder)return void r.push(`${s}: max per order must be 1 to ${X.maxPerOrder}.`);let _=f?ft(f,!1):null;if(f&&!_)return void r.push(`${s}: "${f}" isn't a date (use YYYY-MM-DD).`);let v=p?ft(p,!0):null;if(p&&!v)return void r.push(`${s}: "${p}" isn't a date (use YYYY-MM-DD).`);if(_&&v&&v<=_)return void r.push(`${s}: sales end before they start.`);let y=dt(c);for(let e=2;i.has(y);e++)y=`${dt(c)}_${e}`;i.add(y),n.push({id:y,name:c,priceMinor:m,capacity:h,maxPerOrder:g,..._?{salesStart:_}:{},...v?{salesEnd:v}:{}})}),n.length>X.tiers&&r.push(`At most ${X.tiers} tiers per event.`),{tiers:n.slice(0,X.tiers),errors:r}}function ht(e,t){let n=e&&typeof e==`object`?e:{},r=ot(n.currency),i=new Set,a=[];for(let e of Array.isArray(n.tiers)?n.tiers.slice(0,X.tiers):[]){let t=e??{},n=typeof t.id==`string`&&/^[a-z0-9_]{1,48}$/.test(t.id)&&!i.has(t.id)?t.id:null,r=typeof t.name==`string`?t.name.trim().slice(0,X.name):``,o=(e,t,n,r)=>Number.isSafeInteger(e)&&e>=t&&e<=n?e:r;if(!n||!r||!Number.isSafeInteger(t.priceMinor)||t.priceMinor<0)continue;i.add(n);let s=e=>typeof e==`string`&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():void 0,c=s(t.salesStart),l=s(t.salesEnd);a.push({id:n,name:r,priceMinor:t.priceMinor,capacity:o(t.capacity,0,X.capacity,0),maxPerOrder:o(t.maxPerOrder,1,X.maxPerOrder,X.defaultMaxPerOrder),...c?{salesStart:c}:{},...l?{salesEnd:l}:{}})}let o=Number.isSafeInteger(n.capacity)&&n.capacity>=0?n.capacity:void 0,s=pe(n.questions),c=it(n.invoice);return{poolKey:t,currency:r,tiers:a,...o===void 0?{}:{capacity:o},...s.length?{questions:s}:{},...c?{invoice:c}:{},...n.cardOff===!0?{cardOff:!0}:{},...Number.isSafeInteger(n.selfCancelHours)&&n.selfCancelHours>=0&&n.selfCancelHours<=8760?{selfCancelHours:n.selfCancelHours}:{}}}function gt(e,t,n){return e.salesStart&&n<Date.parse(e.salesStart)?`not_yet`:e.salesEnd&&n>=Date.parse(e.salesEnd)?`ended`:t<=0?`sold_out`:`on_sale`}function _t(e,t){if(e.capacity!==void 0)return e.capacity;let n=Number(t.data?.capacity??0);return Number.isSafeInteger(n)&&n>0?n:0}function vt(e,t,n,r=0){let i=(e?.taken??0)+(e?.held??0),a=t===0?1/0:Math.max(0,t-i);if(!n||r===0)return a;let o=e?.buckets?.[n];return Math.min(a,Math.max(0,r-(o?.taken??0)-(o?.held??0)))}const yt=e=>Number.isFinite(e)?e:null;async function bt(e,t,n,r,i=Date.now()){if(!n)return{ok:!1,error:`MISSING_EVENT`};if(r&&Number.isNaN(Date.parse(r)))return{ok:!1,error:`INVALID_OCCURRENCE`};let a=await J(e.kv,t,i),{eventCollection:o}=a,s=await t.call(()=>e.content.get(o,n));if(!s||s.status!==`published`)return{ok:!1,error:`EVENT_NOT_FOUND`};if(!ue(s))return{ok:!1,error:`EVENT_NOT_TICKETED`};let c=de(s),u=ht(await t.call(()=>e.storage.tier_settings.get(c)),c),d=_t(u,s),f=await t.call(()=>e.kv.getVersioned(m(c,r))),p=f?_(f.value):null,h=u.tiers.map(e=>{let t=vt(p,d,e.id,e.capacity);return{id:e.id,name:e.name,priceMinor:e.priceMinor,price:lt(e.priceMinor,u.currency),seatsLeft:yt(t),maxPerOrder:Math.max(0,Math.min(e.maxPerOrder,t)),status:gt(e,t,i),...e.salesStart?{salesStart:e.salesStart}:{},...e.salesEnd?{salesEnd:e.salesEnd}:{}}}),g=l(s.data??{},r);return{ok:!0,eventId:n,occurrenceStart:r,...g?.startsAt?{startsAt:g.startsAt}:{},...g?{allDay:g.allDay}:{},currency:u.currency,open:u.tiers.length>0&&u.currency!==``,seatsLeft:yt(vt(p,d)),tiers:h,questions:u.questions??[],invoice:u.invoice?.enabled===!0,card:u.cardOff!==!0,...$e(a)?{turnstileSiteKey:a.turnstileSiteKey}:{}}}const xt=new Map;function St(e){let t=0,n=(n,r)=>{let a=[],o=``,s=()=>{o&&a.push(o),o=``};for(;t<e.length;){let c=e[t];if(c===`{`)s(),t++,a.push(i(n));else if(c===`}`&&r)break;else c===`#`&&n?(s(),a.push(0),t++):(o+=c,t++)}return s(),a},r=()=>{for(;e[t]===` `;)t++;let n=t;for(;t<e.length&&!/[\s,{}]/.test(e[t]);)t++;let r=e.slice(n,t);for(;e[t]===` `||e[t]===`
2
2
  `;)t++;return r},i=i=>{let a=r();if(e[t]===`}`)return t++,{a};t++;let o=r();if(o!==`plural`&&o!==`select`){let n=e[t]===`,`?(t++,r()):``;for(;t<e.length&&e[t]!==`}`;)t++;return t++,{a,t:o,s:n}}t++;let s={};for(;;){let a=r();if(!a||e[t]!==`{`)break;t++,s[a]=n(o===`plural`||i,!0),t++}for(;t<e.length&&e[t]!==`}`;)t++;return t++,{a,p:o===`plural`,o:s}};return n(!1,!1)}function Ct(e,t,n,r){let i=``;for(let a of e)if(typeof a==`string`)i+=a;else if(a===0)i+=r===void 0?`#`:new Intl.NumberFormat(n).format(r);else if(a.t)i+=wt(a,t[a.a],n);else if(!a.o)i+=a.a in t?String(t[a.a]):`{${a.a}}`;else{let e=t[a.a],o;if(a.p){let r=Number(e),s=`other`;try{s=new Intl.PluralRules(n).select(r)}catch{}o=a.o[`=`+r]??a.o[s]??a.o.other,i+=Ct(o??[],t,n,r)}else o=a.o[String(e)]??a.o.other,i+=Ct(o??[],t,n,r)}return i}function wt(e,t,n){if(t==null||t===``)return``;try{if(e.t===`number`)return new Intl.NumberFormat(n).format(Number(t));if(e.t===`date`){let r=String(t),i=new Date(/^\d{4}-\d{2}-\d{2}$/.test(r)?r+`T00:00:00Z`:r);return Number.isNaN(i.getTime())?r:new Intl.DateTimeFormat(n,{dateStyle:e.s||`medium`,...r.length===10?{timeZone:`UTC`}:{}}).format(i)}}catch{}return String(t)}function Tt(e,t={},n=`en`){if(!e.includes(`{`))return e;let r=xt.get(e);return r||(r=St(e),xt.size>4e3&&xt.clear(),xt.set(e,r)),Ct(r,t,n)}const Et=[`fr`,`de`,`nl`,`es`,`it`,`pt`,`pt-BR`,`pl`,`cs`,`sv`,`da`,`nb`,`fi`,`ja`,`zh-CN`,`ko`,`tr`,`uk`,`ar`,`he`],Dt={no:`nb`,nn:`nb`,iw:`he`,"zh-Hans":`zh-CN`,"zh-SG":`zh-CN`,"zh-MY":`zh-CN`};function Ot(e,t=Et){let n;try{n=Intl.getCanonicalLocales(String(e||``).trim().replace(/_/g,`-`))[0]}catch{}if(!n)return`en`;let[r,...i]=n.split(`-`),a=i.find(e=>e.length===4),o=i.find(e=>e.length===2||/^\d{3}$/.test(e)),s=[];if(r===`zh`){if(a===`Hant`||!a&&[`TW`,`HK`,`MO`].includes(o))return`en`;s.push(`zh-CN`)}o&&s.push(`${r}-${o}`,Dt[`${r}-${o}`]),a&&s.push(Dt[`${r}-${a}`]),s.push(r,Dt[r]);for(let e of s)if(e===`en`||t.includes(e))return e;return`en`}function kt(e,t,n=`en`,r=``){let i=Array.isArray(e)?[]:{};for(let[a,o]of Object.entries(e)){let e=r+a;if(typeof o==`string`){let r=t?.[e]||o;i[a]=r.includes(`{`)?Tt(r,{},n):r}else if(Array.isArray(o)&&typeof o[0]==`string`){let[r,...s]=o,c=t?.[e]||r;i[a]=s.length?(...e)=>Tt(c,Object.fromEntries(s.map((t,n)=>[t,e[n]])),n):(e={})=>Tt(c,e,n)}else o&&typeof o==`object`?i[a]=kt(o,t,n,e+`.`):i[a]=o}return i}const At=new Map;function jt(e,t){return`https://cdn.jsdelivr.net/npm/${e.package}@${e.version}/catalogs/${e.namespace}/${t}.json`}const Mt=(e,t)=>`i18n:${e.namespace}:${e.version}:${t}`;async function Nt(e){let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e));return btoa(String.fromCharCode(...new Uint8Array(t,0,16))).slice(0,22)}async function Pt(e,t,n){let r=await e.http.fetch(jt(t,n));if(!r.ok)return null;let i=await r.text();if(await Nt(i)!==t.hashes[n])return null;let a=JSON.parse(i);return!a||typeof a!=`object`?null:(await e.kv.set(Mt(t,n),a),At.set(Mt(t,n),{messages:a}),a)}async function Ft(e,t,n,r){let i=Ot(n,Object.keys(t.hashes));if(i===`en`)return null;let a=Mt(t,i),o=At.get(a);if(o&&(o.messages||Date.now()<o.retry))return o.messages;let s=null;try{if(r&&r()<1)return null;if(s=await e.kv?.get(a)??null,!s){if(!e.http||!e.kv||r&&r()<2)return null;s=await Pt(e,t,i)}}catch{s=null}return s&&typeof s!=`object`&&(s=null),At.set(a,s?{messages:s}:{messages:null,retry:Date.now()+3e5}),s}async function It(e,t,n,r){let i=Object.keys(t.hashes);if(!i.length||!e.kv||!e.http||r&&r()<4)return{added:0,missing:i.length};let a=Mt(t,`index`),o=await e.kv.get(a),s=new Set(Array.isArray(o)?o:[]),c=[...new Set([Ot(n,i),...i])].filter(e=>e!==`en`&&!s.has(e)),l=0;for(let n of c){if(r&&r()<3)break;try{await Pt(e,t,n)&&(s.add(n),l++)}catch{}}return l&&await e.kv.set(a,[...s]),{added:l,missing:c.length-l}}const Lt={keyLabels:{missing:`Not saved`,test:`Saved: test mode (sk_test_)`,live:`Saved: LIVE mode (sk_live_)`,"restricted-test":`Saved: restricted key, test mode (rk_test_)`,"restricted-live":`Saved: restricted key, LIVE mode (rk_live_)`,unrecognised:`Saved, but it doesn't look like a Stripe secret key (sk_… or rk_…)`},devHostTitle:`Development host: no licence key needed.`,devHostDescription:[`Running on {hostname} (local or preview host).`,`hostname`],licensedTitle:[`Licensed for {hostname} (updates until {until}).`,`hostname`,`until`],licensedDescription:`Offline signed licence verified for this build.`,licenceExpiredTitle:`Licence expired for this build`,licenceExpiredDescription:[`Your key covers versions released until {until}. This version is newer: renew to keep selling, or reinstall an older version.`,`until`],licenceInvalidTitle:[`{hasStoredKey, select, true {Invalid licence key for {hostname}} other {Licence key required for {hostname}}}`,`hostname`,`hasStoredKey`],thisSite:`this site`,licenceRequiredDescription:[`No valid licence key for {hostname}. New ticket sales are refused until you add one; existing orders keep working.`,`hostname`],licenceExempt:`Licence: development host — no licence key needed.`,licenceActive:[`Licence: active for {hostname} (updates until {until}).`,`hostname`,`until`],licenceExpired:[`Licence: your key covers versions released until {until}. Paste a renewed licence code below and click Activate to keep selling.`,`until`],licenceMissing:[`Licence: {hasStoredKey, select, true {saved key is invalid or doesn't match} other {no key saved for}} {hostname}. Paste your licence code below and click Activate.`,`hostname`,`hasStoredKey`],setupTitle:`Eventual RSVP – Tickets`,encryptionTitle:`Encryption key needed for card payments`,encryptionDescription:"Tickets keeps your Stripe keys as encrypted settings, so card payments need EMDASH_ENCRYPTION_KEY in the site's environment. Generate one with `emdash secrets generate` and add it (on Cloudflare: `wrangler secret put EMDASH_ENCRYPTION_KEY`), then reload this page. Free tickets and pay-by-invoice work without it.",readyLiveTitle:`Ready to sell (live payments)`,readyLiveDescription:`Payments are real. Orders are marked LIVE.`,readyTestTitle:`Ready for test payments`,readyTestDescription:`Stripe is in test mode: no money moves and every order is marked TEST. Switch to a live key when you are ready.`,notReadyTitle:`Not ready for card payments`,notReadyDescription:`Complete the checklist below. Card payments stay off until Stripe is connected; free tickets and pay-by-invoice work without it.`,fieldStripeKey:`Stripe secret key`,fieldWebhookSecret:`Webhook signing secret`,webhookSaved:`Saved (whsec_…)`,webhookNotSaved:`Not saved`,fieldEncryption:`Encryption key`,encryptionPresent:`Present`,fieldSiteUrl:`Site URL`,siteUrlUnset:`Not set: needed for checkout return pages and the webhook URL`,connectStripe:`Connect Stripe`,webhookStep1:[`1. In Stripe, add a webhook endpoint with this URL: {url}`,`url`],webhookStep2:[`2. Subscribe it to: {events}.`,`events`],webhookStep3:`3. Paste the endpoint's signing secret and your secret key in the Tickets settings.`,webhookNoSiteUrl:`Set the site URL first: the webhook URL is built from it.`,openSettings:`Open Tickets settings`,eventsHeader:`Events`,componentsHint:[`Site components (TicketCheckout, TicketCheckoutReturn, TicketOrder, TicketCheckin): use apiBase "/_emdash/api/plugins/{id}".`,`id`],fieldEventCollection:`Event collection (the one chosen in Event settings)`,fieldCheckoutReturnPath:`Checkout return page path (Stripe sends buyers back here)`,fieldOrderPagePath:`Order page path (the ticket email links here: view, show the ticket, cancel)`,fieldRetentionDays:`Keep attendee data for this many days after the event (30–3650)`,fieldDefaultTimezone:`Time zone for events that don't have their own, for example Europe/Zurich (empty: UTC)`,fieldAutoRefund:`Refund paid orders that got no seats, or paid the wrong amount, automatically`,fieldTurnstileSiteKey:`Cloudflare Turnstile site key (optional; empty = off)`,fieldTurnstileSecretKey:`Cloudflare Turnstile secret key`,turnstileSecretSaved:`Saved. Leave empty to keep it.`,fieldLicenceKey:`Licence key`,licenceKeySaved:`Saved. Leave empty to keep it.`,licenceKeyPlaceholder:`evt1.…`,licenceHeader:`Licence`,licenceIntro:[`Paste the licence code from your purchase email and click Activate. Tickets asks licences.eventual.rsvp for a key for {hostname}, once; after that the key is checked on this site, offline.`,`hostname`],fieldLicence:`Licence code (EVT-…) or key (evt1.…)`,licenceCodeSaved:[`Code …{hint} saved. Leave empty to keep it.`,`hint`],activate:`Activate`,activateThisSite:`Activate this site`,refreshKey:`Refresh key`,deactivateSite:`Deactivate this site`,deactivateConfirmTitle:`Deactivate the licence on this site?`,deactivateConfirmText:[`{hostname} stops selling tickets and frees its place on your licence, so you can activate another site. Moves are limited to 3 per site a year.`,`hostname`],deactivate:`Deactivate`,lostCode:`All sites using your code, and a Deactivate for each: licences.eventual.rsvp/manage. Lost your code? licences.eventual.rsvp/resend sends it to the address you bought with.`,activated:[`Licence activated for {hostname}.{seats, plural, =0 {} other { {used} of {seats} sites in use.}}`,`hostname`,`used`,`seats`],activatedExempt:[`Code saved. {hostname} is a development host and needs no key; click Activate this site after moving to your real address.`,`hostname`],keySaved:`Licence key saved.`,deactivated:[`Licence deactivated for {hostname}.{moves, select, none {} other { Moves left this year: {moves}.}}`,`hostname`,`moves`],notALicence:`That is not a licence code (EVT-…) or key (evt1.…).`,noSiteUrl:`Set this site's URL in EmDash first: the licence is tied to its hostname.`,issuerErrors:{CODE_INVALID:`This licence code is not known. Check it against your purchase email.`,CODE_REVOKED:`This licence code was withdrawn (for example after a refund). Reply to your purchase email if this is a mistake.`,HOSTNAME_INVALID:[`{hostname} can't be licensed. Check the site URL in EmDash.`,`hostname`],NO_FREE_SITE:[`All sites of this licence are in use ({hostnames}). Deactivate one of them first, from its own Tickets page.`,`hostnames`],NOT_ACTIVE:`This site was not active on the licence. Its key was removed here.`,MOVES_EXHAUSTED:`This licence has used its moves for this year. Reply to your purchase email and we will help.`,TOO_MANY_REQUESTS:`Too many attempts. Wait a few minutes and try again.`,UNREACHABLE:`The licence service could not be reached. Try again in a moment; nothing was changed.`},save:`Save`,turnstileOn:`Turnstile is on: the checkout form shows Cloudflare's check, and orders without a valid token are refused. If Cloudflare can't be reached, orders go through.`,turnstileOff:`Turnstile is off. To turn it on, create a widget for this site's domain in the Cloudflare dashboard (Turnstile) and paste both keys above.`,tiersHint:`Set up tiers in each event's Tickets panel (in the event form, the field with the choices “Rsvp” and “Tickets” must be set to “Tickets”), and add the TicketCheckout and TicketCheckoutReturn components to your site.`,openOrders:`Open Orders`,scheduledTasks:[`Scheduled background task(s): {names}.`,`names`],savedEvents:[`Saved: events from "{collection}".`,`collection`],openEvent:`Open an event to set up its tickets.`,wrongCollection:[`Tickets are sold for the "{collection}" collection. This can be changed on the Tickets setup page if your events live elsewhere.`,`collection`],saveFirst:`Save the event first, then set up its tickets here.`,noTicketsTitle:`This event doesn't sell tickets`,noTicketsBody:`To sell tickets, set the event's field with the choices “Rsvp” and “Tickets” to “Tickets”, then save the event. Free registration (RSVP) and tickets can't both be used for the same event.`,nothingSaved:`Nothing was saved`,currencyInvalid:[`"{text}" isn't a currency code (use three letters, for example CHF or EUR).`,`text`],invoiceNeedsText:`Pay by invoice needs payment instructions (bank details and the reference to use).`,settingsSaved:`Ticket settings saved.`,tiersNeedFixing:`Ticket tiers need fixing`,tierNotSaved:`The ticket type was not saved`,tierGone:`This ticket type no longer exists. Reload the page.`,tooManyTiers:[`At most {limit} ticket types per event.`,`limit`],tierSaved:`Ticket type saved.`,tierAdded:`Ticket type added.`,tierNotRemoved:`The ticket type was not removed`,tierInUse:[`"{name}" has tickets sold or in checkout, so it stays. To stop selling it, set "On sale until" to a past date.`,`name`],tierRemoved:`Ticket type removed.`,questionNotSaved:`The question was not saved`,questionGone:`This question no longer exists. Reload the page.`,tooManyQuestions:[`At most {limit} questions per event.`,`limit`],questionSaved:`Question saved.`,questionAdded:`Question added.`,questionRemoved:`Question removed.`,nothingSavedSeeTop:`Nothing was saved: see the messages at the top of the panel.`,soldText:[`{n, plural, one {# ticket sold} other {# tickets sold}}`,`n`],heldText:[`{n} in checkout right now`,`n`],capacityText:[`{n, plural, one {# place per date} other {# places per date}}`,`n`],noLimit:`no limit on places`,viewOrders:`View orders`,ticketTypes:`Ticket types`,saveCurrencyFirst:`Save the currency above first; then add your ticket types here.`,tierSoldOf:[`{sold} of {capacity} sold`,`sold`,`capacity`],tierSold:[`{sold} sold`,`sold`],addTicketType:`Add a ticket type`,fieldName:`Name`,namePlaceholder:`For example: Adult`,fieldPrice:`Price (for example 45 or 45.50; write free for a free ticket)`,fieldCapacity:`Tickets of this type per date (0 = no limit of its own)`,fieldMaxPerOrder:`Most tickets in one order`,fieldSaleFrom:`On sale from (optional)`,fieldSaleUntil:`On sale until, that day included (optional)`,addTierButton:`Add ticket type`,saveTierButton:`Save ticket type`,fieldQuestion:`Question`,questionPlaceholder:`For example: T-shirt size`,fieldQuestionType:`Kind of answer`,questionTypes:{text:`Short text`,textarea:`Long text`,email:`Email address`,phone:`Phone number`,number:`Number`,date:`Date`,checkbox:`Tick box`,select:`Choice from a list`},tierNeedsName:`Give the ticket type a name.`,saveCurrencyFirstShort:`Save the currency first (under Ticket settings).`,questionNeedsText:`Write the question first.`,choiceNeedsOptions:`a choice from a list needs at least one choice`,fieldChoices:`Choices (one per line)`,choicesPlaceholder:`S
3
3
  M
4
4
  L`,fieldRequired:`Must be answered`,addQuestionButton:`Add question`,saveQuestionButton:`Save question`,removeLabel:`Remove`,removeTitle:[`Remove "{name}"?`,`name`],removeConfirm:`Remove`,keepConfirm:`Keep`,removeTierText:`Buyers can no longer choose it. A ticket type with sales can't be removed.`,removeQuestionText:`The question disappears from the form. Answers already given stay with their orders.`,questionAccordion:[`{number}. {label} · {type}{required, select, true { · required} other {}}`,`number`,`label`,`type`,`required`],ticketSettings:`Ticket settings`,fieldPlacesPerDate:`Places per date (0 = no limit)`,fieldCurrency:`Currency (three letters, for example CHF)`,selfCancelLabel:`Buyers can cancel their own order`,selfCancelDescription:`From the order page linked in the ticket email. Card payments are refunded in full; unpaid invoices are cancelled.`,selfCancelHoursLabel:`Until this many hours before the event`,cardEnabledLabel:`Online payment (card, TWINT and other Stripe methods)`,cardEnabledDescription:`Turn this off to offer only payment by invoice for this event.`,noWayToPay:`Paid tickets need a way to pay: turn on online payment or payment by invoice.`,invoiceEnabledLabel:`Offer payment by invoice or bank transfer`,invoiceEnabledDescription:`Places are confirmed at once. When the money arrives, mark the order paid under Tickets → Orders; the buyer then gets their ticket.`,invoiceInstructionsLabel:`Payment instructions emailed to the buyer (bank details, reference, deadline)`,invoiceInstructionsPlaceholder:`IBAN CH00 0000 0000 0000 0000 0
@@ -35,9 +35,9 @@ Please quote this reference: {reference}
35
35
  {link}
36
36
 
37
37
  } other {}}Your tickets are emailed as soon as the payment is confirmed.
38
- `]},refund:{subject:[`Your payment for {title} was refunded`],text:[`Your payment for {title}{hasDate, select, true { on {date}} other {}} arrived after the seats were no longer available (or didn't match the order), so we refunded it in full. It can take a few days to appear on your statement.`]}},Xt=new WeakMap;let Zt;function Qt(e,t){if(!e)return Zt??=kt(Yt,null,`en`);let n=Xt.get(e);return n||Xt.set(e,n=kt(Yt,Vt(e,`email.`),t)),n}function $t(e,t){let n={hasDate:!!t.date,hasLink:!!t.link,...t};return{subject:e.subject(n),text:e.text(n)}}async function en(e,t,n,r=1){return Qt(await Gt(e,t,n,r),n)}function tn(e,t,n,r=Yt.free){if(e===0)return r;let i=st(t),a=String(t||`chf`).toUpperCase();try{let t=a===`CHF`&&!n.includes(`-`)?`${n}-CH`:n;return new Intl.NumberFormat(t,{style:`currency`,currency:a}).format(e/10**i).replace(/[  ]/g,` `)}catch{return`${(e/10**i).toFixed(i)} ${String(t||``).toUpperCase()}`}}function nn(e,t,n,r){if(typeof e!=`number`&&(typeof e!=`string`||!e.trim()))return r;let i=Number(e);return Number.isSafeInteger(i)?Math.max(t,Math.min(n,i)):r}const rn=(e,t)=>(typeof e==`string`?e:typeof e==`number`?String(e):``).slice(0,t).trim(),Z=(e,t=200)=>rn(e,t).replace(/\|/g,`/`),an=e=>{let t=e.replace(/^Line \d+: /,``);return t.charAt(0).toUpperCase()+t.slice(1)};function on(e,t,n){if(!e.name)return{error:n.tierNeedsName};if(!t)return{error:n.saveCurrencyFirstShort};let r=mt([e.name.replace(/^#+\s*/,``),e.price,e.capacity,e.maxPerOrder,e.from.slice(0,10),e.until.slice(0,10)].join(` | `),t);return r.errors[0]?{error:an(r.errors[0])}:r.tiers[0]?{tier:r.tiers[0]}:{error:n.tierNeedsName}}function sn(e,t){let n=e.label.replace(/^#+\s*/,``);if(!n)return{error:t.questionNeedsText};let r=e.type===`select`?e.options.split(/[\n,]/).map(e=>Z(e,200)).filter(Boolean).join(`, `):``,i=fe([n,e.type,e.required?`required`:`optional`,r].join(` | `));return i.errors[0]?{error:an(i.errors[0]).replace(`a select question needs options`,t.choiceNeedsOptions)}:{question:i.questions[0]}}function cn(e,t,n){return{type:`form`,block_id:t?`tier-new`:`tier-${e.id}`,fields:[{type:`text_input`,action_id:`name`,label:n.fieldName,placeholder:t?n.namePlaceholder:void 0,initial_value:e.name},{type:`text_input`,action_id:`price`,label:n.fieldPrice,initial_value:e.price},{type:`number_input`,action_id:`capacity`,label:n.fieldCapacity,min:0,max:X.capacity,initial_value:Number(e.capacity)||0},{type:`number_input`,action_id:`maxPerOrder`,label:n.fieldMaxPerOrder,min:1,max:X.maxPerOrder,initial_value:Number(e.maxPerOrder)||X.defaultMaxPerOrder},{type:`date_input`,action_id:`from`,label:n.fieldSaleFrom,initial_value:e.from},{type:`date_input`,action_id:`until`,label:n.fieldSaleUntil,initial_value:e.until}],submit:t?{action_id:`add-tier`,label:n.addTierButton}:{action_id:`save-tier:${e.id}`,label:n.saveTierButton}}}function ln(e,t,n){return{type:`form`,block_id:t?`question-new`:`question-${e.id}`,fields:[{type:`text_input`,action_id:`label`,label:n.fieldQuestion,placeholder:t?n.questionPlaceholder:void 0,initial_value:e.label},{type:`select`,action_id:`type`,label:n.fieldQuestionType,options:H.map(e=>({label:n.questionTypes[e]??e,value:e})),initial_value:e.type},{type:`text_input`,action_id:`options`,label:n.fieldChoices,placeholder:n.choicesPlaceholder,multiline:!0,initial_value:e.options,condition:{field:`type`,eq:`select`}},{type:`toggle`,action_id:`required`,label:n.fieldRequired,initial_value:e.required}],submit:t?{action_id:`add-question`,label:n.addQuestionButton}:{action_id:`save-question:${e.id}`,label:n.saveQuestionButton}}}const un=(e,t,n,r,i)=>({type:`button`,action_id:e,label:i.removeLabel,style:`danger`,value:t,confirm:{title:i.removeTitle(n),text:r,confirm:i.removeConfirm,deny:i.keepConfirm}});async function dn(e,t){let n=new r,i=await Gt(t,n,e.ui?.locale),a=Bt(i,e.ui?.locale),o=Ot(e.ui?.locale),s=Qt(i,o).free,c=e.ui?.entry;if(!c?.id)return{blocks:[{type:`context`,text:a.openEvent}]};let l=t.kv,{eventCollection:u}=await Y(l,n);if(c.collection!==u)return{blocks:[{type:`context`,text:a.wrongCollection(u)}]};let d=await n.call(()=>t.content.get(c.collection,c.id));if(!d)return{blocks:[{type:`context`,text:a.saveFirst}]};let f=[];if(!ue(d))return f.push({type:`banner`,title:a.noTicketsTitle,description:a.noTicketsBody,variant:`default`}),{blocks:f};let m=de(d),h=t.storage.tier_settings,g=ht(await n.call(()=>h.get(m)),m),v=e.input&&typeof e.input==`object`?e.input:{},y=typeof v.action_id==`string`?v.action_id:``,b=v.values&&typeof v.values==`object`?v.values:null,x=`capacity`in(d.data??{}),S,C=[],w=a.nothingSaved,T=null,E=null,D=g.currency,O=g.selfCancelHours!==void 0,k=g.selfCancelHours??48,A=g.cardOff!==!0,j=g.invoice?.enabled===!0,M=g.invoice?.instructions??``,N=_t(g,d),P=async(e,t)=>{let r={...e,updatedAt:new Date().toISOString()};await n.call(()=>h.put(m,r)),g=ht(r,m),S={message:t,type:`success`}},F=`${p}${m}`,I=(await n.call(()=>l.list(F))).filter(e=>e.key===F||e.key.startsWith(`${F}:`)).map(e=>_(e.value)).filter(e=>!!e),L=(e,t)=>I.reduce((n,r)=>n+(r.buckets?.[e]?.[t]??0),0);if(y===`save-ticket-settings`&&b){N=`capacity`in b?nn(b.capacity,0,1e5,N):N,D=`currency`in b?rn(b.currency,20):g.currency;let e=ot(D);D&&!e&&C.push(a.currencyInvalid(D));let t=g.tiers;if(typeof b.tiers==`string`){let n=mt(b.tiers.slice(0,2e4),e);C.push(...n.errors),t=n.tiers}let n=g.questions??[];if(typeof b.questions==`string`){let e=fe(b.questions.slice(0,2e4));C.push(...e.errors.map(e=>`Questions, ${e.charAt(0).toLowerCase()}${e.slice(1)}`)),n=e.questions}O=b.selfCancel===!0,k=nn(b.selfCancelHours,0,8760,k),`cardEnabled`in b&&(A=b.cardEnabled===!0),j=b.invoiceEnabled===!0,M=typeof b.invoiceInstructions==`string`?b.invoiceInstructions.slice(0,rt):M,j&&!M.trim()&&C.push(a.invoiceNeedsText),!A&&!j&&t.some(e=>e.priceMinor>0)&&C.push(a.noWayToPay);let r=it({enabled:j,instructions:M});C.length===0?(await P({poolKey:m,...`capacity`in b||!x?{capacity:N}:{},currency:e,tiers:t,...n.length?{questions:n}:{},...r?{invoice:r}:{},...A?{}:{cardOff:!0},...O?{selfCancelHours:k}:{}},a.settingsSaved),N=_t(g,d)):typeof b.tiers==`string`&&(w=a.tiersNeedFixing)}else if((y===`add-tier`||y.startsWith(`save-tier:`))&&b){let e=y===`add-tier`?``:y.slice(10),t=[...g.tiers],n=t.findIndex(t=>t.id===e),r={id:e,name:Z(b.name),price:Z(b.price,40),capacity:Z(b.capacity,12),maxPerOrder:Z(b.maxPerOrder,12),from:Z(b.from,40),until:Z(b.until,40)},i=on(r,g.currency,a);if(w=a.tierNotSaved,e&&n<0)C=[a.tierGone];else if(!e&&t.length>=X.tiers)C=[a.tooManyTiers(X.tiers)];else if(!i.tier)C=[i.error],T=r;else if(e)t[n]={...i.tier,id:e},await P({...g,tiers:t},a.tierSaved);else{let e=new Set(t.map(e=>e.id)),n=i.tier.id;for(let t=2;e.has(n);t++)n=`${i.tier.id}_${t}`;t.push({...i.tier,id:n}),await P({...g,tiers:t},a.tierAdded)}}else if(y===`remove-tier`){let e=g.tiers.find(e=>e.id===v.value);w=a.tierNotRemoved,e?L(e.id,`taken`)+L(e.id,`held`)>0?C=[a.tierInUse(e.name)]:await P({...g,tiers:g.tiers.filter(t=>t.id!==e.id)},a.tierRemoved):C=[a.tierGone]}else if((y===`add-question`||y.startsWith(`save-question:`))&&b){let e=y===`add-question`?``:y.slice(14),t=[...g.questions??[]],n=t.findIndex(t=>t.id===e),r={id:e,label:Z(b.label,400),type:rn(b.type,20)||`text`,required:b.required===!0,options:typeof b.options==`string`?b.options.slice(0,4e3):``},i=sn(r,a);if(w=a.questionNotSaved,e&&n<0)C=[a.questionGone];else if(!e&&t.length>=U.questions)C=[a.tooManyQuestions(U.questions)];else if(!i.question)C=[i.error],E=r;else if(e)t[n]={...i.question,id:e},await P({...g,questions:t},a.questionSaved);else{let e=new Set(t.map(e=>e.id)),n=i.question.id;for(let t=2;e.has(n);t++)n=`${i.question.id}_${t}`;t.push({...i.question,id:n}),await P({...g,questions:t},a.questionAdded)}}else if(y===`remove-question`){let e=(g.questions??[]).filter(e=>e.id!==v.value);e.length===(g.questions??[]).length?C=[a.questionGone]:await P({...g,questions:e},a.questionRemoved)}C.length&&(S={message:C.length===1?C[0]:a.nothingSavedSeeTop,type:`error`},f.push({type:`banner`,title:w,description:C.slice(0,10).join(` `),variant:`error`}));let ee=I.reduce((e,t)=>e+t.taken,0),R=I.reduce((e,t)=>e+t.held,0);f.push({type:`section`,text:[a.soldText(ee),R?a.heldText(R):``,N>0?a.capacityText(N):a.noLimit].filter(Boolean).join(` · `),accessory:{type:`link`,label:a.viewOrders,target:{kind:`plugin-page`,path:`/orders`}}});let z=[{type:`section`,text:a.ticketTypes}];if(g.currency||z.push({type:`context`,text:a.saveCurrencyFirst}),g.tiers.forEach(e=>{let t=T?.id===e.id,n=t?T:{id:e.id,name:e.name,price:ut(e.priceMinor,g.currency),capacity:String(e.capacity),maxPerOrder:String(e.maxPerOrder),from:pt(e.salesStart,!1).slice(0,10),until:pt(e.salesEnd,!0).slice(0,10)},r=L(e.id,`taken`);z.push({type:`accordion`,label:[e.name,tn(e.priceMinor,g.currency,o,s),e.capacity>0?a.tierSoldOf(r,e.capacity):a.tierSold(r)].join(` · `),default_open:t,blocks:[cn(n,!1,a),{type:`actions`,elements:[un(`remove-tier`,e.id,e.name,a.removeTierText,a)]}]})}),g.tiers.length<X.tiers){let e=T&&!T.id?T:{id:``,name:``,price:``,capacity:`0`,maxPerOrder:String(X.defaultMaxPerOrder),from:``,until:``};z.push({type:`accordion`,label:a.addTicketType,default_open:g.tiers.length===0&&!!g.currency||!!T&&!T.id,blocks:[cn(e,!0,a)]})}let B=[];x||B.push({type:`number_input`,action_id:`capacity`,label:a.fieldPlacesPerDate,min:0,max:1e5,initial_value:N}),B.push({type:`text_input`,action_id:`currency`,label:a.fieldCurrency,initial_value:D.toUpperCase()},{type:`toggle`,action_id:`selfCancel`,label:a.selfCancelLabel,description:a.selfCancelDescription,initial_value:O},{type:`number_input`,action_id:`selfCancelHours`,label:a.selfCancelHoursLabel,min:0,max:8760,initial_value:k,condition:{field:`selfCancel`,eq:!0}},{type:`toggle`,action_id:`cardEnabled`,label:a.cardEnabledLabel,description:a.cardEnabledDescription,initial_value:A},{type:`toggle`,action_id:`invoiceEnabled`,label:a.invoiceEnabledLabel,description:a.invoiceEnabledDescription,initial_value:j},{type:`text_input`,action_id:`invoiceInstructions`,label:a.invoiceInstructionsLabel,placeholder:a.invoiceInstructionsPlaceholder,multiline:!0,initial_value:M,condition:{field:`invoiceEnabled`,eq:!0}});let te=[{type:`section`,text:a.ticketSettings},...x?[{type:`context`,text:a.capacityFromEvent(N>0?a.capacityText(N):a.noLimitPlaces,g.capacity!=null&&Number(g.capacity)!==(Number(d.data?.capacity)||0))}]:[],{type:`form`,block_id:`ticket-settings`,fields:B,submit:{action_id:`save-ticket-settings`,label:a.saveTicketSettings}},{type:`context`,text:a.settingsSaveHint}];f.push(...g.currency?[...z,{type:`divider`},...te]:[...te,{type:`divider`},...z]),f.push({type:`divider`},{type:`section`,text:a.questionsHeader},{type:`context`,text:a.questionsIntro});let ne=g.questions??[];if(ne.forEach((e,t)=>{let n=E?.id===e.id,r=n?E:{id:e.id,label:e.label,type:e.type,required:e.required,options:(e.options??[]).join(`
38
+ `]},refund:{subject:[`Your payment for {title} was refunded`],text:[`Your payment for {title}{hasDate, select, true { on {date}} other {}} arrived after the seats were no longer available (or didn't match the order), so we refunded it in full. It can take a few days to appear on your statement.`]}},Xt=new WeakMap;let Zt;function Qt(e,t){if(!e)return Zt??=kt(Yt,null,`en`);let n=Xt.get(e);return n||Xt.set(e,n=kt(Yt,Vt(e,`email.`),t)),n}function $t(e,t){let n={hasDate:!!t.date,hasLink:!!t.link,...t};return{subject:e.subject(n),text:e.text(n)}}async function en(e,t,n,r=1){return Qt(await Gt(e,t,n,r),n)}function tn(e,t,n,r=Yt.free){if(e===0)return r;let i=st(t),a=String(t||`chf`).toUpperCase();try{let t=a===`CHF`&&!n.includes(`-`)?`${n}-CH`:n;return new Intl.NumberFormat(t,{style:`currency`,currency:a}).format(e/10**i).replace(/[  ]/g,` `)}catch{return`${(e/10**i).toFixed(i)} ${String(t||``).toUpperCase()}`}}function nn(e,t,n,r){if(typeof e!=`number`&&(typeof e!=`string`||!e.trim()))return r;let i=Number(e);return Number.isSafeInteger(i)?Math.max(t,Math.min(n,i)):r}const rn=(e,t)=>(typeof e==`string`?e:typeof e==`number`?String(e):``).slice(0,t).trim(),Z=(e,t=200)=>rn(e,t).replace(/\|/g,`/`),an=e=>{let t=e.replace(/^Line \d+: /,``);return t.charAt(0).toUpperCase()+t.slice(1)};function on(e,t,n){if(!e.name)return{error:n.tierNeedsName};if(!t)return{error:n.saveCurrencyFirstShort};let r=mt([e.name.replace(/^#+\s*/,``),e.price,e.capacity,e.maxPerOrder,e.from.slice(0,10),e.until.slice(0,10)].join(` | `),t);return r.errors[0]?{error:an(r.errors[0])}:r.tiers[0]?{tier:r.tiers[0]}:{error:n.tierNeedsName}}function sn(e,t){let n=e.label.replace(/^#+\s*/,``);if(!n)return{error:t.questionNeedsText};let r=e.type===`select`?e.options.split(/[\n,]/).map(e=>Z(e,200)).filter(Boolean).join(`, `):``,i=fe([n,e.type,e.required?`required`:`optional`,r].join(` | `));return i.errors[0]?{error:an(i.errors[0]).replace(`a select question needs options`,t.choiceNeedsOptions)}:{question:i.questions[0]}}function cn(e,t,n){return{type:`form`,block_id:t?`tier-new`:`tier-${e.id}`,fields:[{type:`text_input`,action_id:`name`,label:n.fieldName,placeholder:t?n.namePlaceholder:void 0,initial_value:e.name},{type:`text_input`,action_id:`price`,label:n.fieldPrice,initial_value:e.price},{type:`number_input`,action_id:`capacity`,label:n.fieldCapacity,min:0,max:X.capacity,initial_value:Number(e.capacity)||0},{type:`number_input`,action_id:`maxPerOrder`,label:n.fieldMaxPerOrder,min:1,max:X.maxPerOrder,initial_value:Number(e.maxPerOrder)||X.defaultMaxPerOrder},{type:`date_input`,action_id:`from`,label:n.fieldSaleFrom,initial_value:e.from},{type:`date_input`,action_id:`until`,label:n.fieldSaleUntil,initial_value:e.until}],submit:t?{action_id:`add-tier`,label:n.addTierButton}:{action_id:`save-tier:${e.id}`,label:n.saveTierButton}}}function ln(e,t,n){return{type:`form`,block_id:t?`question-new`:`question-${e.id}`,fields:[{type:`text_input`,action_id:`label`,label:n.fieldQuestion,placeholder:t?n.questionPlaceholder:void 0,initial_value:e.label},{type:`select`,action_id:`type`,label:n.fieldQuestionType,options:H.map(e=>({label:n.questionTypes[e]??e,value:e})),initial_value:e.type},{type:`text_input`,action_id:`options`,label:n.fieldChoices,placeholder:n.choicesPlaceholder,multiline:!0,initial_value:e.options,condition:{field:`type`,eq:`select`}},{type:`toggle`,action_id:`required`,label:n.fieldRequired,initial_value:e.required}],submit:t?{action_id:`add-question`,label:n.addQuestionButton}:{action_id:`save-question:${e.id}`,label:n.saveQuestionButton}}}const un=(e,t,n,r,i)=>({type:`button`,action_id:e,label:i.removeLabel,style:`danger`,value:t,confirm:{title:i.removeTitle(n),text:r,confirm:i.removeConfirm,deny:i.keepConfirm}});async function dn(e,t){let n=new r,i=await Gt(t,n,e.ui?.locale),a=Bt(i,e.ui?.locale),o=Ot(e.ui?.locale),s=Qt(i,o).free,c=e.ui?.entry;if(!c?.id)return{blocks:[{type:`context`,text:a.openEvent}]};let l=t.kv,{eventCollection:u}=await J(l,n);if(c.collection!==u)return{blocks:[{type:`context`,text:a.wrongCollection(u)}]};let d=await n.call(()=>t.content.get(c.collection,c.id));if(!d)return{blocks:[{type:`context`,text:a.saveFirst}]};let f=[];if(!ue(d))return f.push({type:`banner`,title:a.noTicketsTitle,description:a.noTicketsBody,variant:`default`}),{blocks:f};let m=de(d),h=t.storage.tier_settings,g=ht(await n.call(()=>h.get(m)),m),v=e.input&&typeof e.input==`object`?e.input:{},y=typeof v.action_id==`string`?v.action_id:``,b=v.values&&typeof v.values==`object`?v.values:null,x=`capacity`in(d.data??{}),S,C=[],w=a.nothingSaved,T=null,E=null,D=g.currency,O=g.selfCancelHours!==void 0,k=g.selfCancelHours??48,A=g.cardOff!==!0,j=g.invoice?.enabled===!0,M=g.invoice?.instructions??``,N=_t(g,d),P=async(e,t)=>{let r={...e,updatedAt:new Date().toISOString()};await n.call(()=>h.put(m,r)),g=ht(r,m),S={message:t,type:`success`}},F=`${p}${m}`,I=(await n.call(()=>l.list(F))).filter(e=>e.key===F||e.key.startsWith(`${F}:`)).map(e=>_(e.value)).filter(e=>!!e),L=(e,t)=>I.reduce((n,r)=>n+(r.buckets?.[e]?.[t]??0),0);if(y===`save-ticket-settings`&&b){N=`capacity`in b?nn(b.capacity,0,1e5,N):N,D=`currency`in b?rn(b.currency,20):g.currency;let e=ot(D);D&&!e&&C.push(a.currencyInvalid(D));let t=g.tiers;if(typeof b.tiers==`string`){let n=mt(b.tiers.slice(0,2e4),e);C.push(...n.errors),t=n.tiers}let n=g.questions??[];if(typeof b.questions==`string`){let e=fe(b.questions.slice(0,2e4));C.push(...e.errors.map(e=>`Questions, ${e.charAt(0).toLowerCase()}${e.slice(1)}`)),n=e.questions}O=b.selfCancel===!0,k=nn(b.selfCancelHours,0,8760,k),`cardEnabled`in b&&(A=b.cardEnabled===!0),j=b.invoiceEnabled===!0,M=typeof b.invoiceInstructions==`string`?b.invoiceInstructions.slice(0,rt):M,j&&!M.trim()&&C.push(a.invoiceNeedsText),!A&&!j&&t.some(e=>e.priceMinor>0)&&C.push(a.noWayToPay);let r=it({enabled:j,instructions:M});C.length===0?(await P({poolKey:m,...`capacity`in b||!x?{capacity:N}:{},currency:e,tiers:t,...n.length?{questions:n}:{},...r?{invoice:r}:{},...A?{}:{cardOff:!0},...O?{selfCancelHours:k}:{}},a.settingsSaved),N=_t(g,d)):typeof b.tiers==`string`&&(w=a.tiersNeedFixing)}else if((y===`add-tier`||y.startsWith(`save-tier:`))&&b){let e=y===`add-tier`?``:y.slice(10),t=[...g.tiers],n=t.findIndex(t=>t.id===e),r={id:e,name:Z(b.name),price:Z(b.price,40),capacity:Z(b.capacity,12),maxPerOrder:Z(b.maxPerOrder,12),from:Z(b.from,40),until:Z(b.until,40)},i=on(r,g.currency,a);if(w=a.tierNotSaved,e&&n<0)C=[a.tierGone];else if(!e&&t.length>=X.tiers)C=[a.tooManyTiers(X.tiers)];else if(!i.tier)C=[i.error],T=r;else if(e)t[n]={...i.tier,id:e},await P({...g,tiers:t},a.tierSaved);else{let e=new Set(t.map(e=>e.id)),n=i.tier.id;for(let t=2;e.has(n);t++)n=`${i.tier.id}_${t}`;t.push({...i.tier,id:n}),await P({...g,tiers:t},a.tierAdded)}}else if(y===`remove-tier`){let e=g.tiers.find(e=>e.id===v.value);w=a.tierNotRemoved,e?L(e.id,`taken`)+L(e.id,`held`)>0?C=[a.tierInUse(e.name)]:await P({...g,tiers:g.tiers.filter(t=>t.id!==e.id)},a.tierRemoved):C=[a.tierGone]}else if((y===`add-question`||y.startsWith(`save-question:`))&&b){let e=y===`add-question`?``:y.slice(14),t=[...g.questions??[]],n=t.findIndex(t=>t.id===e),r={id:e,label:Z(b.label,400),type:rn(b.type,20)||`text`,required:b.required===!0,options:typeof b.options==`string`?b.options.slice(0,4e3):``},i=sn(r,a);if(w=a.questionNotSaved,e&&n<0)C=[a.questionGone];else if(!e&&t.length>=U.questions)C=[a.tooManyQuestions(U.questions)];else if(!i.question)C=[i.error],E=r;else if(e)t[n]={...i.question,id:e},await P({...g,questions:t},a.questionSaved);else{let e=new Set(t.map(e=>e.id)),n=i.question.id;for(let t=2;e.has(n);t++)n=`${i.question.id}_${t}`;t.push({...i.question,id:n}),await P({...g,questions:t},a.questionAdded)}}else if(y===`remove-question`){let e=(g.questions??[]).filter(e=>e.id!==v.value);e.length===(g.questions??[]).length?C=[a.questionGone]:await P({...g,questions:e},a.questionRemoved)}C.length&&(S={message:C.length===1?C[0]:a.nothingSavedSeeTop,type:`error`},f.push({type:`banner`,title:w,description:C.slice(0,10).join(` `),variant:`error`}));let ee=I.reduce((e,t)=>e+t.taken,0),R=I.reduce((e,t)=>e+t.held,0);f.push({type:`section`,text:[a.soldText(ee),R?a.heldText(R):``,N>0?a.capacityText(N):a.noLimit].filter(Boolean).join(` · `),accessory:{type:`link`,label:a.viewOrders,target:{kind:`plugin-page`,path:`/orders`}}});let z=[{type:`section`,text:a.ticketTypes}];if(g.currency||z.push({type:`context`,text:a.saveCurrencyFirst}),g.tiers.forEach(e=>{let t=T?.id===e.id,n=t?T:{id:e.id,name:e.name,price:ut(e.priceMinor,g.currency),capacity:String(e.capacity),maxPerOrder:String(e.maxPerOrder),from:pt(e.salesStart,!1).slice(0,10),until:pt(e.salesEnd,!0).slice(0,10)},r=L(e.id,`taken`);z.push({type:`accordion`,label:[e.name,tn(e.priceMinor,g.currency,o,s),e.capacity>0?a.tierSoldOf(r,e.capacity):a.tierSold(r)].join(` · `),default_open:t,blocks:[cn(n,!1,a),{type:`actions`,elements:[un(`remove-tier`,e.id,e.name,a.removeTierText,a)]}]})}),g.tiers.length<X.tiers){let e=T&&!T.id?T:{id:``,name:``,price:``,capacity:`0`,maxPerOrder:String(X.defaultMaxPerOrder),from:``,until:``};z.push({type:`accordion`,label:a.addTicketType,default_open:g.tiers.length===0&&!!g.currency||!!T&&!T.id,blocks:[cn(e,!0,a)]})}let B=[];x||B.push({type:`number_input`,action_id:`capacity`,label:a.fieldPlacesPerDate,min:0,max:1e5,initial_value:N}),B.push({type:`text_input`,action_id:`currency`,label:a.fieldCurrency,initial_value:D.toUpperCase()},{type:`toggle`,action_id:`selfCancel`,label:a.selfCancelLabel,description:a.selfCancelDescription,initial_value:O},{type:`number_input`,action_id:`selfCancelHours`,label:a.selfCancelHoursLabel,min:0,max:8760,initial_value:k,condition:{field:`selfCancel`,eq:!0}},{type:`toggle`,action_id:`cardEnabled`,label:a.cardEnabledLabel,description:a.cardEnabledDescription,initial_value:A},{type:`toggle`,action_id:`invoiceEnabled`,label:a.invoiceEnabledLabel,description:a.invoiceEnabledDescription,initial_value:j},{type:`text_input`,action_id:`invoiceInstructions`,label:a.invoiceInstructionsLabel,placeholder:a.invoiceInstructionsPlaceholder,multiline:!0,initial_value:M,condition:{field:`invoiceEnabled`,eq:!0}});let te=[{type:`section`,text:a.ticketSettings},...x?[{type:`context`,text:a.capacityFromEvent(N>0?a.capacityText(N):a.noLimitPlaces,g.capacity!=null&&Number(g.capacity)!==(Number(d.data?.capacity)||0))}]:[],{type:`form`,block_id:`ticket-settings`,fields:B,submit:{action_id:`save-ticket-settings`,label:a.saveTicketSettings}},{type:`context`,text:a.settingsSaveHint}];f.push(...g.currency?[...z,{type:`divider`},...te]:[...te,{type:`divider`},...z]),f.push({type:`divider`},{type:`section`,text:a.questionsHeader},{type:`context`,text:a.questionsIntro});let ne=g.questions??[];if(ne.forEach((e,t)=>{let n=E?.id===e.id,r=n?E:{id:e.id,label:e.label,type:e.type,required:e.required,options:(e.options??[]).join(`
39
39
  `)};f.push({type:`accordion`,label:a.questionAccordion(t+1,e.label,a.questionTypes[e.type]??e.type,e.required),default_open:n,blocks:[ln(r,!1,a),{type:`actions`,elements:[un(`remove-question`,e.id,e.label,a.removeQuestionText,a)]}]})}),ne.length<U.questions){let e=E&&!E.id?E:{id:``,label:``,type:`text`,required:!1,options:``};f.push({type:`accordion`,label:a.addQuestionHeader,default_open:!!E&&!E.id,blocks:[ln(e,!0,a)]})}return{blocks:f,toast:S}}async function fn(e,t){return`ord_${(await L(`order:${e}:${t}`)).slice(0,32)}`}function Q(e,t=`en`){let n=e.startsAt||e.occurrenceStart||``;if(!n)return``;if(e.allDay)try{return new Intl.DateTimeFormat(t,{dateStyle:`medium`,timeZone:e.timezone||`UTC`}).format(new Date(n))}catch{return new Intl.DateTimeFormat(t,{dateStyle:`medium`,timeZone:`UTC`}).format(new Date(n))}return d(n,e.timezone,t)}function pn(e){return{ok:!0,orderId:e.id,status:e.status,quantity:e.quantity,totalMinor:e.totalMinor,currency:e.currency,...e.mode?{mode:e.mode}:{},...e.status===`pending`&&e.checkoutUrl?{checkoutUrl:e.checkoutUrl}:{},...e.status===`pending`&&e.holdExpiresAt?{holdExpiresAt:e.holdExpiresAt}:{}}}function mn(e,t){return e.eventId!==t.eventId||e.occurrenceStart!==t.occurrenceStart||e.buyerEmail!==t.buyerEmail?!1:e.lines.map(e=>`${e.tierId}:${e.quantity}`).sort().join(`,`)===t.lines.map(e=>`${e.tierId}:${e.quantity}`).sort().join(`,`)}function hn(e){let t=[],n=(e,r)=>{if(r!=null){if(Array.isArray(r))return r.forEach((t,r)=>n(`${e}[${r}]`,t));if(typeof r==`object`)return Object.entries(r).forEach(([t,r])=>n(`${e}[${t}]`,r));t.push(`${encodeURIComponent(e)}=${encodeURIComponent(String(r))}`)}};return Object.entries(e).forEach(([e,t])=>n(e,t)),t.join(`&`)}async function gn(e,t,n,r,i,a,o){let s;try{s=await t.call(()=>e.fetch(`https://api.stripe.com/v1${i}`,{method:r,headers:{authorization:`Bearer ${n}`,"stripe-version":`2024-06-20`,...a?{"content-type":`application/x-www-form-urlencoded`}:{},...o?{"idempotency-key":o}:{}},...a?{body:hn(a)}:{},redirect:`manual`}))}catch{return{ok:!1,error:`NETWORK`}}let c=await s.json().catch(()=>null);return!s.ok||!c||typeof c!=`object`?{ok:!1,error:`STRIPE`,status:s.status,...typeof c?.error?.type==`string`?{type:c.error.type}:{},...typeof c?.error?.code==`string`?{code:c.error.code}:{}}:{ok:!0,data:c}}function _n(e,t,n,r){return gn(e,t,n,`POST`,`/checkout/sessions`,{mode:`payment`,client_reference_id:r.orderId,customer_email:r.customerEmail,expires_at:r.expiresAt,success_url:r.successUrl,cancel_url:r.cancelUrl,metadata:{order_id:r.orderId,event_id:r.eventId},payment_intent_data:{metadata:{order_id:r.orderId}},line_items:r.lines.map(e=>({quantity:e.quantity,price_data:{currency:r.currency,unit_amount:e.unitAmountMinor,product_data:{name:e.name}}}))},`checkout-${r.orderId}`)}function vn(e,t,n,r,i,a=`requested_by_customer`){return gn(e,t,n,`POST`,`/refunds`,{payment_intent:r,reason:a,metadata:{order_id:i}},`refund-${i}`)}function yn(e){return Array.from(new Uint8Array(e),e=>e.toString(16).padStart(2,`0`)).join(``)}function bn(e,t){if(e.length!==t.length)return!1;let n=0;for(let r=0;r<e.length;r++)n|=e.charCodeAt(r)^t.charCodeAt(r);return n===0}async function xn(e,t,n,r=Math.floor(Date.now()/1e3),i=300){if(!t)return{ok:!1,reason:`MISSING`};let a=-1,o=[];for(let e of t.split(`,`)){let[t,n]=e.split(`=`);t===`t`&&(a=Number.parseInt(n??``,10)),t===`v1`&&n&&o.push(n)}if(!Number.isFinite(a)||a<0||o.length===0)return{ok:!1,reason:`MALFORMED`};let s=await crypto.subtle.importKey(`raw`,new TextEncoder().encode(n),{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]),c=new TextEncoder().encode(`${a}.`),l=typeof e==`string`?new TextEncoder().encode(e):e,u=new Uint8Array(c.length+l.length);u.set(c),u.set(l,c.length);let d=yn(await crypto.subtle.sign(`HMAC`,s,u));return o.some(e=>bn(e,d))?i>0&&r-a>i?{ok:!1,reason:`STALE`}:{ok:!0,timestamp:a}:{ok:!1,reason:`BAD_SIGNATURE`}}function Sn(e,t){let n=Number(t?.amount_total),r=String(t?.currency??``).toLowerCase();return Number.isSafeInteger(n)&&n===e.totalMinor&&r===e.currency.toLowerCase()}function Cn(e,t){return I(e,`ticket`,[t.id,t.occurrenceStart])}function wn(e,t){return{[t]:e.quantity,buckets:Object.fromEntries(e.lines.map(e=>[e.tierId,{[t]:e.quantity}]))}}function Tn(e,t){return I(e,`manage`,[t])}async function En(e,t){return t.orderPageUrl?`${t.orderPageUrl}#order=${t.id}&token=${await Tn(e,t.id)}`:``}async function Dn(e,t,n,r,i){if(!n.buyerEmail)return`none`;let o=i?await En(i,n):``;if(!e.email?.send||!t.has(1))return`none`;let s=Jt(n.locale),c=await en(e,t,s),l=n.lines.map(e=>{let t=e.unitPriceMinor*e.quantity;return` ${e.quantity} × ${e.tierName}${t>0?` ${tn(t,n.currency,s)}`:``}`}).join(`
40
- `),u=$t(c.ticket,{name:n.buyerName,title:n.eventTitle,date:Q(n,s),lines:l,total:tn(n.totalMinor,n.currency,s,c.free),code:r,link:o,orderId:n.id,quantity:n.quantity,test:n.mode===`test`});try{return await t.call(()=>e.email.send({to:n.buyerEmail,...u})),`sent`}catch(r){return a(t,e.log,`warn`,`Ticket confirmation email failed`,{orderId:n.id,error:String(r)}),`none`}}async function On(e,t,n,r=Date.now()){let i=typeof n?.type==`string`?n.type:``,o=n?.data?.object??{};if(i===`charge.refunded`)return kn(e,t,o,r);if(!i.startsWith(`checkout.session.`))return{status:200,result:`IGNORED`};let s=String(o.metadata?.order_id??o.client_reference_id??``);if(!/^ord_[0-9a-f]{32}$/.test(s))return{status:200,result:`UNKNOWN_ORDER`};let c=await t.call(()=>e.storage.orders.get(s));if(!c)return{status:200,result:`UNKNOWN_ORDER`};if(c.stripeSessionId&&o.id!==c.stripeSessionId)return a(t,e.log,`warn`,`Stripe event for a different session than the order's`,{orderId:s}),{status:200,result:`SESSION_MISMATCH`};let l=new Date(r).toISOString(),u=m(c.poolKey,c.occurrenceStart);if(i===`checkout.session.completed`&&o.payment_status===`paid`||i===`checkout.session.completed`&&o.payment_status===`no_payment_required`||i===`checkout.session.async_payment_succeeded`){if(!Sn(c,o)){let n=typeof o.payment_intent==`string`?o.payment_intent:void 0,r={status:`paid`,paidAt:l,updatedAt:l,needsAttention:`AMOUNT_MISMATCH`,...n?{stripePaymentIntentId:n}:{}};return c.status===`pending`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:r}))).applied?(await C(e.kv,t,u,wn(c,`held`),1),a(t,e.log,`error`,`Stripe payment amount or currency differs from the order`,{orderId:c.id}),{status:200,result:`AMOUNT_MISMATCH`}):{status:200,result:`ALREADY_DONE`}:c.status===`expired`&&(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`expired`},set:r}))).applied?(a(t,e.log,`error`,`Stripe payment amount or currency differs from the order`,{orderId:c.id}),{status:200,result:`AMOUNT_MISMATCH`}):{status:200,result:`ALREADY_DONE`}}let n=await B(e.kv,t),r=await Cn(n,c),i=typeof o.payment_intent==`string`?o.payment_intent:void 0,s={status:`paid`,paidAt:l,updatedAt:l,ticketCodeHash:await L(r),...i?{stripePaymentIntentId:i}:{}},d=await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:s}));if(d.applied)return await C(e.kv,t,u,wn(c,`heldToTaken`),1),await Dn(e,t,d.data,r,n),{status:200,result:`PAID`};if(c.status===`expired`&&t.has(5)){let i=ht(await t.call(()=>e.storage.tier_settings.get(c.poolKey)),c.poolKey),o=await t.call(()=>e.kv.getVersioned(u)),l=i.capacity??_(o?.value)?.capacity??0,d=await T(e.kv,t,u,{capacity:l,lines:c.lines.map(e=>({bucket:e.tierId,quantity:e.quantity,bucketCapacity:i.tiers.find(t=>t.id===e.tierId)?.capacity??0})),poolKey:c.poolKey,occurrenceStart:c.occurrenceStart,...c.timezone?{timezone:c.timezone}:{},confirm:!0,maxAttempts:1}),f=d.ok?`PAID_AFTER_EXPIRY`:`PAID_NO_SEATS`,p=d.ok?{...s,needsAttention:f}:{...s,ticketCodeHash:void 0,needsAttention:f},m=t.has(1)?await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`expired`},set:p})):{applied:!1};return m.applied?(a(t,e.log,d.ok?`warn`:`error`,`Stripe payment arrived after the seat hold expired`,{orderId:c.id,needsAttention:f}),d.ok&&await Dn(e,t,m.data,r,n),{status:200,result:f}):{status:200,result:`ALREADY_DONE`}}return{status:200,result:`ALREADY_DONE`}}return i===`checkout.session.completed`&&o.payment_status===`unpaid`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:{holdExpiresAt:new Date(r+6048e5).toISOString(),updatedAt:l}})),{status:200,result:`AWAITING_PAYMENT`}):i===`checkout.session.expired`||i===`checkout.session.async_payment_failed`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:{status:`expired`,updatedAt:l}}))).applied?(await C(e.kv,t,u,wn(c,`held`),1),{status:200,result:`EXPIRED`}):{status:200,result:`ALREADY_DONE`}:{status:200,result:`IGNORED`}}async function kn(e,t,n,r){let i=typeof n?.payment_intent==`string`?n.payment_intent:``,a=Number(n?.amount_refunded);if(!/^pi_[A-Za-z0-9_]+$/.test(i)||!Number.isSafeInteger(a)||a<=0)return{status:200,result:`IGNORED`};let o=String(n?.metadata?.order_id??``),s=null;if(/^ord_[0-9a-f]{32}$/.test(o)&&(s=await t.call(()=>e.storage.orders.get(o))),s||=(await t.call(()=>e.storage.orders.query({where:{stripePaymentIntentId:i},limit:1}))).items[0]?.data??null,!s||s.paymentMethod!==`stripe`)return{status:200,result:`UNKNOWN_ORDER`};if(s.stripePaymentIntentId&&s.stripePaymentIntentId!==i)return{status:200,result:`SESSION_MISMATCH`};if(s.status===`pending`||s.status===`expired`)return{status:503,reason:`REFUND_BEFORE_PAYMENT`};if(s.status!==`paid`||a<=(s.refundedMinor??0))return{status:200,result:`ALREADY_DONE`};let c=new Date(r).toISOString(),l=n?.refunded===!0||a>=s.totalMinor,u=l?{status:`refunded`,refundedMinor:a,refundedAt:c,updatedAt:c}:{refundedMinor:a,updatedAt:c},d=s;return(await t.call(()=>e.storage.orders.updateIf(d.id,{where:{status:`paid`},set:u}))).applied?l?(d.needsAttention!==`PAID_NO_SEATS`&&await C(e.kv,t,m(d.poolKey,d.occurrenceStart),wn(d,`taken`),1),{status:200,result:`REFUNDED`}):{status:200,result:`PARTIALLY_REFUNDED`}:{status:200,result:`ALREADY_DONE`}}async function An(e,t,n,i=Date.now()){let a=new r,o=String(await a.call(()=>n.settings.get(we))??``).trim();if(!o.startsWith(`whsec_`))return{status:503,reason:`WEBHOOK_NOT_CONFIGURED`};let s=await xn(e,t,o,Math.floor(i/1e3));if(!s.ok)return{status:400,reason:s.reason};let c;try{c=JSON.parse(typeof e==`string`?e:new TextDecoder(`utf-8`,{fatal:!0}).decode(e))}catch{return{status:400,reason:`MALFORMED_JSON`}}return On(n,a,c,i)}async function jn(e,t){return(await I(e,`ip`,[t])).slice(0,32)}async function Mn(e,t,n,r=Date.now()){return(await t.call(()=>e.query({where:{ipHash:n,createdAt:{gt:new Date(r-6e5).toISOString()}},limit:10}))).items.length>=10}const Nn=Xe({publicKey:`NP1DWBqz-d5wgkCaoKbedn5gB1mmjyi2d-ZF5iJPT1c`,buildDate:`2026-11-14`});let Pn=null;async function Fn(e,t,n=Date.now()){let r=He(t),i=`${e}\0${r}`;if(Pn&&Pn.cacheKey===i&&n>=Pn.at&&n-Pn.at<6e4)return Pn.result;let a=await Nn({key:e,hostname:r});return Pn={cacheKey:i,result:a,at:n},a}async function In(e,t,n,r,i=``){if(!n.buyerEmail||!e.email?.send||!t.has(1))return`none`;let o=Jt(n.locale),s=await en(e,t,o),c=n.lines.map(e=>{let t=e.unitPriceMinor*e.quantity;return` ${e.quantity} × ${e.tierName}${t>0?` ${tn(t,n.currency,o)}`:``}`}).join(`
41
- `),l=$t(s.invoice,{name:n.buyerName,title:n.eventTitle,date:Q(n,o),lines:c,totalDue:tn(n.totalMinor,n.currency,o,s.free),instructions:r,reference:n.id,link:i});try{return await t.call(()=>e.email.send({to:n.buyerEmail,...l})),`sent`}catch(r){return a(t,e.log,`warn`,`Invoice instructions email failed`,{orderId:n.id,error:String(r)}),`none`}}const $=(e,t={})=>({ok:!1,error:e,...t});function Ln(e){if(!Array.isArray(e)||e.length===0||e.length>20)return null;let t=new Set,n=[];for(let r of e){let e=r??{},i=typeof e.tierId==`string`?e.tierId:``,a=Number(e.quantity);if(!/^[a-z0-9_]{1,48}$/.test(i)||t.has(i)||!Number.isSafeInteger(a)||a<1)return null;t.add(i),n.push({tierId:i,quantity:a})}return n}async function Rn(e,t,n=Date.now(),i=null){let a=new r,o=e&&typeof e==`object`?e:{};if(!ce(typeof o.honeypot==`string`?o.honeypot:void 0))return{ok:!0,status:`pending`};if(!le(typeof o.formTimestamp==`number`?o.formTimestamp:void 0,n))return $(`INVALID_SUBMISSION_TIMING`);if(!se(o.commandId))return $(`MISSING_COMMAND_ID`);let s=String(o.eventId??``).trim().slice(0,128),c=String(o.occurrenceStart??``).trim().slice(0,40),d=String(o.buyerName??``).trim().slice(0,100),f=oe(o.buyerEmail),p=Jt(o.locale);if(!s||!d||!o.buyerEmail)return $(`MISSING_REQUIRED_FIELDS`);if(!f)return $(`INVALID_EMAIL`);if(c&&Number.isNaN(Date.parse(c)))return $(`INVALID_OCCURRENCE`);let h=o.paymentMethod===`invoice`,g=ee(o.returnKeyHash)?o.returnKeyHash:``,_=Ln(o.lines);if(!_)return $(`INVALID_LINES`);let v=_.reduce((e,t)=>e+t.quantity,0);if(v>50)return $(`TOO_MANY_SEATS`,{max:50});let y=await Y(t.kv,a,n),b=await Fn(y.licenceKey,t.site?.url,n);if(!b.ok)return $(b.error===`LICENCE_EXPIRED`?`LICENCE_EXPIRED`:`LICENCE_REQUIRED`);let x=await fn(s,o.commandId),S=await a.call(()=>t.storage.orders.get(x));if(S)return mn(S,{eventId:s,occurrenceStart:c,buyerEmail:f,lines:_})?{...pn(S),replayed:!0}:$(`IDEMPOTENCY_CONFLICT`);if(Qe(y)){if(!t.http)return $(`TRY_AGAIN`);if(!(await Se((e,n)=>t.http.fetch(e,n),a,{secret:y.turnstileSecretKey,token:o.turnstileToken,remoteIp:i})).ok)return $(`CHALLENGE_FAILED`)}let w=await a.call(()=>t.content.get(y.eventCollection,s));if(!w||w.status!==`published`)return $(`EVENT_NOT_FOUND`);if(!ue(w))return $(`EVENT_NOT_TICKETED`);let E=de(w),D=ht(await a.call(()=>t.storage.tier_settings.get(E)),E);if(!D.currency||D.tiers.length===0)return $(`NOT_ON_SALE`);let O=[],k=[];for(let e of _){let t=D.tiers.find(t=>t.id===e.tierId);if(!t)return $(`UNKNOWN_TIER`,{tierId:e.tierId});let r=gt(t,1/0,n);if(r!==`on_sale`)return $(`TIER_NOT_ON_SALE`,{tierId:t.id,status:r});if(e.quantity>t.maxPerOrder)return $(`MAX_PER_ORDER_EXCEEDED`,{tierId:t.id,max:t.maxPerOrder});O.push({tierId:t.id,tierName:t.name,quantity:e.quantity,unitPriceMinor:t.priceMinor}),k.push({bucket:t.id,quantity:e.quantity,bucketCapacity:t.capacity})}let A=O.reduce((e,t)=>e+t.unitPriceMinor*t.quantity,0),j=he(D.questions??[],o.answers);if(!j.ok)return $(`INVALID_ANSWERS`,{fields:j.errors});let M=(h||D.cardOff===!0)&&A>0;if(M&&!D.invoice?.enabled)return $(`INVOICE_NOT_AVAILABLE`);let N=A===0||M,P=String(t.site?.url??``).replace(/\/+$/,``),F=``;if(A>0&&!M){F=String(await a.call(()=>t.settings.get(Ce))??``).trim();let e=J(F);if(e===`missing`||e===`unrecognised`||!/^https?:\/\//.test(P)||!t.http)return $(`PAYMENTS_NOT_CONFIGURED`)}let I=A===0?await B(t.kv,a):``,R=I?await Cn(I,{id:x,occurrenceStart:c}):``,z=I,te=typeof i==`string`?i.trim():``,ne=N?3:4,re=N?1:0,ie=``;if(!Qe(y)&&te&&(!z&&a.has(ne+re+2)&&(z=await B(t.kv,a)),z&&a.has(ne+re+1)&&(ie=await jn(z,te),await Mn(t.storage.orders,a,ie,n))))return $(`RATE_LIMITED`);let ae=String(w.data?.title??`Event`).slice(0,200),V=l(w.data??{},c),H=u(V?.timezone)??u(w.data?.timezone)??(y.defaultTimezone||void 0),U=m(E,c),W=await T(t.kv,a,U,{capacity:_t(D,w),lines:k,poolKey:E,occurrenceStart:c,label:ae,...H?{timezone:H}:{},confirm:N,maxAttempts:a.has(6)?2:1});if(!W.ok)return W.reason===`FULL`?$(`SOLD_OUT`):W.reason===`BUCKET_FULL`?$(`TIER_SOLD_OUT`,{tierId:W.bucket}):$(`TRY_AGAIN`);let G=new Date(n).toISOString(),fe={id:x,commandId:o.commandId,eventId:s,poolKey:E,occurrenceStart:c,...V?.startsAt?{startsAt:V.startsAt}:{},...V?{allDay:V.allDay}:{},...H?{timezone:H}:{},locale:p,eventTitle:ae,buyerName:d,buyerEmail:f,lines:O,quantity:v,totalMinor:A,currency:D.currency,status:A===0?`paid`:M?`pending_invoice`:`pending`,paymentMethod:A===0?`free`:M?`invoice`:`stripe`,...j.answers.length?{answers:j.answers}:{},...ie?{ipHash:ie}:{},...g&&A>0&&!M?{returnKeyHash:g}:{},.../^https?:\/\//.test(P)?{orderPageUrl:`${P}${y.orderPagePath}`}:{},retainFrom:V?.startsAt||c||G,createdAt:G,...A===0?{paidAt:G}:{},updatedAt:G},pe=async e=>{if(!a.has(2))return;let n=e?`taken`:`held`;await C(t.kv,a,U,{[n]:v,buckets:Object.fromEntries(O.map(e=>[e.tierId,{[n]:e.quantity}]))},1)},K=R?{...fe,ticketCodeHash:await L(R)}:fe;if(A>0&&!M){let e=Q({startsAt:V?.startsAt,occurrenceStart:c,timezone:H,allDay:V?.allDay},p),r=Math.floor(n/1e3)+1860,i=e=>`${P}${y.checkoutReturnPath}?result=${e}&order=${x}`,o=await _n(t.http,a,F,{orderId:x,eventId:s,currency:D.currency,customerEmail:f,successUrl:i(`success`),cancelUrl:i(`cancelled`),expiresAt:r,lines:O.map(t=>({name:`${ae} · ${t.tierName}${e?` · ${e}`:``}`.slice(0,250),unitAmountMinor:t.unitPriceMinor,quantity:t.quantity}))});if(!o.ok||!o.data.url)return await pe(!1),$(`PAYMENT_UNAVAILABLE`);K={...fe,mode:Ee(J(F))?`live`:`test`,stripeSessionId:o.data.id,checkoutUrl:o.data.url,holdExpiresAt:new Date((o.data.expires_at||r)*1e3+3e5).toISOString()}}if(!a.has(1))return $(`TRY_AGAIN`);if(!(await a.call(()=>t.storage.orders.compareAndSet(x,null,K))).applied){await pe(N);let e=a.has(1)?await a.call(()=>t.storage.orders.get(x)):null;return e?{...pn(e),replayed:!0}:$(`TRY_AGAIN`)}let me=R?await Dn(t,a,K,R,I):`none`;if(M){let e=z||(a.has(3)?await B(t.kv,a):``),n=e?await En(e,K):``,r=await In(t,a,K,D.invoice.instructions,n);return{...pn(K),instructions:D.invoice.instructions,email:r,...n?{orderPageUrl:n}:{}}}let ge=I?await En(I,K):``;return{...pn(K),...N?{email:me,...ge?{orderPageUrl:ge}:{}}:{}}}const zn=/^ord_[0-9a-f]{32}$/,Bn=/^[0-9a-f]{64}$/;function Vn(e){return{taken:e.quantity,buckets:Object.fromEntries(e.lines.map(e=>[e.tierId,{taken:e.quantity}]))}}function Hn(e,t){let n=e.startsAt||e.occurrenceStart;if(t===void 0||!n)return null;let r=Date.parse(n);return Number.isNaN(r)?null:new Date(r-t*36e5).toISOString()}function Un(e,t,n){return!t||n>=Date.parse(t)||e.checkedInAt||e.refundRequestedAt?!1:e.status===`pending_invoice`?!0:e.status!==`paid`||e.needsAttention===`PAID_NO_SEATS`?!1:e.paymentMethod===`free`||e.paymentMethod===`stripe`&&!!e.stripePaymentIntentId&&!e.refundedMinor}async function Wn(e,t,n,r){let i=e.status===`paid`&&e.needsAttention!==`PAID_NO_SEATS`;return{ok:!0,orderId:e.id,status:e.status,eventTitle:e.eventTitle,occurrenceStart:e.startsAt||e.occurrenceStart,...e.allDay?{allDay:!0}:{},...e.timezone?{timezone:e.timezone}:{},locale:e.locale??`en`,buyerName:e.buyerName,lines:e.lines.map(e=>({tierName:e.tierName,quantity:e.quantity,priceMinor:e.unitPriceMinor*e.quantity})),quantity:e.quantity,totalMinor:e.totalMinor,currency:e.currency,...e.mode?{mode:e.mode}:{},...i?{ticketCode:await Cn(t,e)}:{},...e.checkedInAt?{checkedInAt:e.checkedInAt}:{},...e.refundRequestedAt?{refundRequestedAt:e.refundRequestedAt}:{},...e.refundedMinor?{refundedMinor:e.refundedMinor}:{},canCancel:Un(e,n,r),...n?{cancelUntil:n}:{}}}async function Gn(e,t,n,r=Date.now()){let i=n&&typeof n==`object`?n:{},o=String(i.orderId??``),s=String(i.token??``).toLowerCase();if(!zn.test(o)||!Bn.test(s))return{ok:!1,error:`INVALID_LINK`};let c=await B(e.kv,t);if(await L(s)!==await L(await Tn(c,o)))return{ok:!1,error:`INVALID_LINK`};let l=await t.call(()=>e.storage.orders.get(o));if(!l)return{ok:!1,error:`INVALID_LINK`};let u=Hn(l,ht(await t.call(()=>e.storage.tier_settings.get(l.poolKey)),l.poolKey).selfCancelHours);if(i.action!==`cancel`)return Wn(l,c,u,r);if(!Un(l,u,r))return{ok:!1,error:`CANNOT_CANCEL`,order:await Wn(l,c,u,r)};let d=new Date(r).toISOString();if(l.paymentMethod===`stripe`){let n=String(await t.call(()=>e.settings.get(Ce))??``).trim();if([`missing`,`unrecognised`].includes(J(n))||!e.http)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let i=await vn(e.http,t,n,l.stripePaymentIntentId,l.id);if(!i.ok)return a(t,e.log,`warn`,`Buyer refund request failed`,{orderId:l.id,error:i.error,status:i.status}),{ok:!1,error:`REFUND_UNAVAILABLE`};let o=t.has(1)?await t.call(()=>e.storage.orders.updateIf(l.id,{where:{status:`paid`},set:{refundRequestedAt:d,updatedAt:d}})):{applied:!1};return{...await Wn(o.applied?o.data:{...l,refundRequestedAt:d},c,u,r),cancelled:!0}}let f=l.status,p=await t.call(()=>e.storage.orders.updateIf(l.id,{where:{status:f},set:{status:`cancelled`,updatedAt:d}}));return p.applied?(await C(e.kv,t,m(l.poolKey,l.occurrenceStart),Vn(l),1),{...await Wn(p.data,c,u,r),cancelled:!0}):{ok:!1,error:`CANNOT_CANCEL`}}async function Kn(e,t,n,r=Date.now()){if(!zn.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(i?.status===`refunded`||i?.refundRequestedAt)return{ok:!0,already:!0};if(!i||i.status!==`paid`||i.paymentMethod!==`stripe`||!i.stripePaymentIntentId)return{ok:!1,error:`NOT_REFUNDABLE`};let a=String(await t.call(()=>e.settings.get(Ce))??``).trim();if([`missing`,`unrecognised`].includes(J(a))||!e.http)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let o=await vn(e.http,t,a,i.stripePaymentIntentId,i.id);if(!o.ok)return{ok:!1,error:o.error===`STRIPE`&&o.code===`charge_already_refunded`?`ALREADY_REFUNDED`:`REFUND_UNAVAILABLE`};let s=new Date(r).toISOString();return await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`paid`},set:{refundRequestedAt:s,updatedAt:s}})),{ok:!0,already:!1}}function qn(e,t=Date.now()){if(!e)return!0;let n=Date.parse(e);return Number.isNaN(n)?!1:n>=t-432e5}async function Jn(e,t,n,r=Date.now()){return(await t.call(()=>e.orders.query({where:{eventId:n,status:`paid`},limit:20}))).items.some(e=>e.data.needsAttention!==`PAID_NO_SEATS`&&e.data.needsAttention!==`AMOUNT_MISMATCH`&&qn(e.data.startsAt||e.data.occurrenceStart,r))?!0:t.has(1)?(await t.call(()=>e.orders.query({where:{eventId:n,status:`pending`},limit:20}))).items.some(e=>qn(e.data.startsAt||e.data.occurrenceStart,r))?!0:t.has(1)?(await t.call(()=>e.orders.query({where:{eventId:n,status:`pending_invoice`},limit:20}))).items.some(e=>qn(e.data.startsAt||e.data.occurrenceStart,r)):!1:!1}const Yn=`/orders`,Xn=`export:`,Zn=/^ord_[0-9a-f]{32}$/;function Qn(e,t){return{[t]:e.quantity,buckets:Object.fromEntries(e.lines.map(e=>[e.tierId,{[t]:e.quantity}]))}}async function $n(e,t,n,r=Date.now()){if(!Zn.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i||i.status!==`pending_invoice`)return{ok:!1,error:`NOT_AWAITING_PAYMENT`};let a=await B(e.kv,t),o=await Cn(a,i),s=new Date(r).toISOString(),c=await L(o),l=await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`pending_invoice`},set:{status:`paid`,paidAt:s,updatedAt:s,ticketCodeHash:c}}));return l.applied?(await Dn(e,t,l.data,o,a),{ok:!0,order:l.data}):{ok:!1,error:`NOT_AWAITING_PAYMENT`}}async function er(e,t,n,r=Date.now()){if(!Zn.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i||i.status!==`pending_invoice`)return{ok:!1,error:`NOT_AWAITING_PAYMENT`};let a=new Date(r).toISOString();return(await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`pending_invoice`},set:{status:`cancelled`,updatedAt:a}}))).applied?(await C(e.kv,t,m(i.poolKey,i.occurrenceStart),Qn(i,`taken`),1),{ok:!0}):{ok:!1,error:`NOT_AWAITING_PAYMENT`}}const tr=[`pending`,`paid`,`pending_invoice`,`expired`,`cancelled`,`refunded`];function nr(e){let t=e&&typeof e==`object`?e:{},n=typeof t.pool==`string`&&/^[A-Za-z0-9_-]{1,128}$/.test(t.pool)?t.pool:void 0,r=n&&typeof t.occurrence==`string`&&!Number.isNaN(Date.parse(t.occurrence))?t.occurrence:void 0,i=tr.includes(t.status)?t.status:void 0;return{...n?{pool:n}:{},...r?{occurrence:r}:{},...i?{status:i}:{},...t.closed===!0?{closed:!0}:{}}}function rr(e){let t={};return e.pool&&(t.poolKey=e.pool),e.pool&&e.occurrence&&(t.occurrenceStart=e.occurrence),e.status&&(t.status=e.status),Object.keys(t).length?t:void 0}function ir(e){let t=e==null?``:String(e);return/^[=+\-@\t\r\uFF1D\uFF0B\uFF0D\uFF20]/.test(t)&&(t=`'${t}`),`"${t.replace(/"/g,`""`)}"`}function ar(e){let t=new Uint8Array(e);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,`0`)).join(``)}const or=`eventual-tickets`;async function sr(e,t,n,r,i=or){let a=n.replace(/\/+$/,``);if(!/^https?:\/\//.test(a))return null;let o=ar(32),s=new Date(Date.now()+6e5).toISOString();return await t.call(async()=>e.set(`${Xn}${await L(o)}`,{...r,expiresAt:s})),{url:`${a}/_emdash/api/plugins/${i||or}/orders/download?token=${o}`,expiresAt:s}}async function cr(e,t,n){if(!ee(n))return null;let r=`${Xn}${await L(n)}`,i=await t.call(()=>e.kv.getVersioned(r));return!i||!(await t.call(()=>e.kv.compareAndDelete(r,i.revision))).applied||!(Date.now()<Date.parse(i.value.expiresAt))?null:lr(e,t,nr(i.value))}async function lr(e,t,n){let r=[],i,a=!1;do{if(!t.has(1)){a=!0;break}let o=await t.call(()=>e.storage.orders.query({where:rr(n),orderBy:{createdAt:`desc`},limit:100,cursor:i}));r.push(...o.items.map(e=>e.data)),i=o.hasMore?o.cursor:void 0}while(i);let o=[[`Order`,`Created`,`Status`,`Mode`,`Payment`,`Buyer`,`Email`,`Event`,`Date`,`Local time`,`Tickets`,`Quantity`,`Total`,`Refunded`,`Paid at`,`Needs attention`,`Answers`].map(ir).join(`,`)];a&&o.unshift(ir(`First ${r.length} orders only: filter by event to export the rest.`));for(let e of r)o.push([e.id,e.createdAt,e.status,e.mode??``,e.paymentMethod,e.erasedAt?`—`:e.buyerName,e.erasedAt?`—`:e.buyerEmail,e.eventTitle,e.startsAt||e.occurrenceStart||`Single event`,e.startsAt||e.occurrenceStart?Q(e,`en`):`Single event`,e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`; `),e.quantity,lt(e.totalMinor,e.currency),e.refundedMinor?lt(e.refundedMinor,e.currency):``,e.paidAt??``,e.needsAttention??``,ge(e.answers)].map(ir).join(`,`));return{filename:`orders-${(n.pool??`all`).toLowerCase().replace(/[^a-z0-9_-]+/g,`-`).slice(0,40)}-${Date.now()}.csv`,body:o.join(`\r
42
- `)}}function ur(e,t,n,r){return e?d(e,t,n):r.singleEvent}function dr(e,t){let n=t.statusLabel[e.status]??e.status,r=e.status===`paid`&&e.refundedMinor?t.partRefunded:``,i=e.status===`paid`&&e.refundRequestedAt&&!e.refundedMinor?t.refundRequested:``,a=e.checkedInAt?t.checkedIn:``;return`${e.mode===`test`?t.testPrefix:``}${n}${r}${i}${a}`}function fr(e,t,n){if(e.status===`paid`&&e.paymentMethod===`stripe`&&e.stripePaymentIntentId&&!e.refundRequestedAt&&!e.refundedMinor)return{type:`button`,action_id:`refund-order`,label:t.refund,style:`danger`,value:e.id,confirm:{title:t.refundTitle(e.buyerName),text:t.refundText(n(e.totalMinor,e.currency)),confirm:t.refund,deny:t.keep}};if(!(e.status!==`pending_invoice`||e.erasedAt))return{type:`button`,action_id:`mark-invoice-paid`,label:t.markPaid,value:e.id,confirm:{title:t.markPaidTitle(e.buyerName),text:t.markPaidText(n(e.totalMinor,e.currency)),confirm:t.markPaidConfirm,deny:t.notYet}}}function pr(e,t){if(!(e.status!==`pending_invoice`||e.erasedAt))return{type:`button`,action_id:`cancel-invoice-order`,label:t.cancel,style:`danger`,value:e.id,confirm:{title:t.cancelTitle(e.buyerName),text:t.cancelText,confirm:t.cancelConfirm,deny:t.keep}}}function mr(e,t,n,r,i,a,o){return{type:`table`,page_action_id:t,...n?{next_cursor:n}:{},columns:[{key:`created`,label:i.columnOrdered},{key:`buyer`,label:i.columnBuyer},{key:`event`,label:i.columnEvent},{key:`date`,label:i.columnDate},{key:`tickets`,label:i.columnTickets},{key:`total`,label:i.columnTotal},{key:`status`,label:i.columnStatus,format:`badge`},{key:`action`,label:i.columnActions,format:`element`},{key:`cancel`,label:` `,format:`element`}],rows:e.map(e=>({created:ur(e.createdAt,e.timezone,a,i),buyer:e.erasedAt?`—`:`${e.buyerName} <${e.buyerEmail}>`,event:e.eventTitle,date:e.startsAt||e.occurrenceStart?Q(e,a):i.singleEvent,tickets:[e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),e.erasedAt?``:ge(e.answers)].filter(Boolean).join(` · `),total:o(e.totalMinor,e.currency),status:e.erasedAt?`${dr(e,i)} · ${i.erased.toLowerCase()}`:dr(e,i),action:fr(e,i,o),cancel:pr(e,i)})),empty_text:r}}function hr(e,t,n,r){return e.flatMap(e=>{let i=[fr(e,t,r),pr(e,t)].filter(e=>!!e);return[{type:`section`,text:[e.erasedAt?`—`:e.buyerName,r(e.totalMinor,e.currency),e.erasedAt?`${dr(e,t)} · ${t.erased.toLowerCase()}`:dr(e,t),e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),e.eventTitle,e.startsAt||e.occurrenceStart?Q(e,n):``,t.orderedAt(ur(e.createdAt,e.timezone,n,t)),e.erasedAt?``:e.buyerEmail,e.erasedAt?``:ge(e.answers)].filter(Boolean).join(` · `)},...i.length?[{type:`actions`,elements:i}]:[]]})}function gr(e){return{type:`actions`,elements:[{type:`link`,label:e.navSetup,target:{kind:`plugin-page`,path:`/tickets`}},{type:`link`,label:e.navOrders,target:{kind:`plugin-page`,path:Yn}}]}}function _r(e){return`state:orders-filter:${e?.id??`shared`}`}function vr(e){if(typeof e==`string`&&e)return e;if(e&&typeof e==`object`&&typeof e.cursor==`string`)return e.cursor}async function yr(e,t,n,i){let a=new r,o=await Gt(t,a,i),s=Bt(o,i),c=Ot(i),l=Qt(o,c).free,u=(e,t)=>tn(e,t,c,l),d=t,f=e&&typeof e==`object`&&!Array.isArray(e)?e:{},m=String(f.action_id??``),g,v,y=[],b,x=[];if(f.page===`widget:recent`){let e=mr((await a.call(()=>d.storage.orders.query({where:{status:{in:[`paid`,`pending_invoice`]}},orderBy:{createdAt:`desc`},limit:5}))).items.map(e=>e.data),`page-recent`,void 0,s.noOrdersYet,s,c,u);return e.columns=e.columns.filter(e=>/^(buyer|event|total|status)$/.test(e.key)),{blocks:[e,{type:`actions`,elements:[{type:`link`,label:s.allOrders,target:{kind:`plugin-page`,path:Yn}}]}]}}if(f.type===`block_action`&&m===`mark-invoice-paid`&&typeof f.value==`string`){let e=await $n(d,a,f.value);g=e.ok?{message:s.markedPaidToast(e.order.buyerName),type:`success`}:{message:s.noLongerAwaiting,type:`info`}}if(f.type===`block_action`&&m===`cancel-invoice-order`&&typeof f.value==`string`&&(g=(await er(d,a,f.value)).ok?{message:s.orderCancelled,type:`success`}:{message:s.noLongerAwaiting,type:`info`}),f.type===`block_action`&&m===`refund-order`&&typeof f.value==`string`){let e=await Kn(t,a,f.value);g=e.ok?{message:e.already?s.alreadyRefunded:s.refundRequestedToast,type:`success`}:{message:e.error===`REFUND_UNAVAILABLE`?s.refundUnavailable:s.refundNotPossible,type:`error`}}let S=m===`toggle-closed-orders`;if(f.type===`form_submit`?m===`apply-orders-filter`:f.type===`block_action`&&(S||m===`clear-orders-filter`||m===`view-event-orders`)){let e=nr(await a.call(()=>d.kv.get(_r(n)))),[t,r]=String(f.value??``).split(`
43
- `);v=nr({...S?e:m===`view-event-orders`?{pool:t,occurrence:r}:f.values,closed:S?!e.closed:e.closed}),await a.call(()=>d.kv.set(_r(n),v))}if(f.type===`form_submit`&&m===`find-personal-data`){let e=oe(f.values?.eraseEmail);if(e){b=e;let t=await a.call(()=>d.storage.orders.query({where:{buyerEmail:e},limit:20})),n=[],r=!1,i=Date.now();for(let e of t.items){let t=e.data,a=(t.status===`pending`||t.status===`pending_invoice`||t.status===`paid`)&&qn(t.startsAt||t.occurrenceStart,i);!a&&t.retainFrom!==`~erased`&&(r=!0),n.push({title:t.eventTitle||t.id,date:t.startsAt||t.occurrenceStart?Q(t,c):s.singleEvent,status:a?s.liveNotErased:t.retainFrom===`~erased`?s.erased:s.statusLabel[t.status]??t.status})}x.push({type:`table`,page_action_id:`page-personal-data`,columns:[{key:`title`,label:s.columnEventOrder},{key:`date`,label:s.columnDate},{key:`status`,label:s.columnStatus,format:`badge`}],rows:n,empty_text:s.noOrdersForEmail}),r&&x.push({type:`actions`,elements:[{type:`button`,action_id:`erase-personal-data`,label:s.erasePersonalData,style:`danger`,value:e,confirm:{title:s.eraseTitle,text:s.eraseText,confirm:s.eraseConfirm,deny:s.keep}}]})}}if(f.type===`block_action`&&m===`erase-personal-data`){let e=oe(f.value);if(e){let t=await a.call(()=>d.storage.orders.query({where:{buyerEmail:e},limit:20})),n=Date.now(),r=new Date(n).toISOString(),i=0,o=t.items.map(e=>e.data).filter(e=>e.retainFrom!==`~erased`&&!((e.status===`pending`||e.status===`pending_invoice`||e.status===`paid`)&&qn(e.startsAt||e.occurrenceStart,n))),c=o.slice(0,6);for(let e of c){if(!a.has(1))break;(await a.call(()=>d.storage.orders.updateIf(e.id,{where:{buyerEmail:e.buyerEmail},set:{buyerName:``,buyerEmail:``,answers:[],ipHash:``,retainFrom:`~erased`,erasedAt:r}}))).applied&&i++}if(g=o.length>i?{message:s.erasedMore(i),type:`info`}:{message:s.erasedAll(i),type:`success`},!a.has(5))return{toast:g,blocks:[gr(s),{type:`header`,text:s.ordersHeader},{type:`context`,text:s.doneReopen}]}}}if(!a.has(5))return{toast:g,blocks:[gr(s),{type:`header`,text:s.ordersHeader},{type:`context`,text:s.doneReopen}]};if(v??=nr(await a.call(()=>d.kv.get(_r(n)))),f.type===`block_action`&&m===`prepare-orders-export`){let e=await sr(d.kv,a,String(t.site?.url??``),v,t.plugin?.id);y.push(e?{type:`actions`,elements:[{type:`link`,label:s.downloadOrders,target:{kind:`external`,url:e.url},appearance:`primary`}]}:{type:`banner`,title:s.siteUrlNotSet,description:s.siteUrlNotSetBody,variant:`error`},{type:`context`,text:s.linkWorksOnce})}let C=await a.call(()=>d.storage.orders.query({where:{needsAttention:`PAID_NO_SEATS`,status:`paid`},limit:20})),w=await a.call(()=>d.storage.orders.query({where:{status:`pending_invoice`},orderBy:{createdAt:`asc`},limit:20})),T=await a.call(()=>d.kv.list(p)),E=m===`page-orders`?vr(f.value):void 0,D=!v.status&&!v.closed,O=f.type===`form_submit`&&m===`apply-orders-filter`?oe(f.values?.buyer):``,k=await a.call(()=>d.storage.orders.query(O?{where:{buyerEmail:O},limit:20}:{where:D?{...rr(v),status:{in:tr.slice(0,3).concat(`cancelled`)}}:rr(v),orderBy:{createdAt:`desc`},limit:50,cursor:E})),A=a.has(1)?await a.call(()=>d.storage.orders.query({where:{needsAttention:`AMOUNT_MISMATCH`,status:`paid`},limit:20})):{items:[],hasMore:!1},j=new Map,M=[],N=[],P=new Map;for(let{key:e,value:t}of T){let n=_(t);if(!n)continue;let r=n.poolKey||h(e,n.occurrenceStart);j.has(r)||j.set(r,n.label||r),(!v.pool||r===v.pool)&&M.push({pool:r,label:n.label||r,ledger:n}),r===v.pool&&n.occurrenceStart&&(N.push(n.occurrenceStart),n.timezone&&P.set(n.occurrenceStart,n.timezone))}N.sort(),M.sort((e,t)=>e.label.localeCompare(t.label)||e.ledger.occurrenceStart.localeCompare(t.ledger.occurrenceStart));let F=new Map;if(M.length&&d.storage.tier_settings&&a.has(1)){let e=await a.call(()=>d.storage.tier_settings.query({...v.pool?{where:{poolKey:v.pool}}:{},limit:100}));for(let{data:t}of e.items)for(let e of t.tiers??[])F.set(`${t.poolKey}\n${e.id}`,e.name)}let I=M.some(e=>e.ledger.occurrenceStart),L=[gr(s),{type:`header`,text:s.ordersHeader}];if(C.items.length){let e=C.items.filter(e=>!!e.data.refundRequestedAt).length;L.push({type:`banner`,title:s.attentionTitle(C.items.length,C.hasMore),description:e===C.items.length?s.attentionAllRefundPending:e>0?s.attentionSomeRefundPending(e):s.attentionNoRefundPending,variant:`error`},mr(C.items.map(e=>e.data),`page-attention`,void 0,``,s,c,u))}if(A.items.length){let e=A.items.filter(e=>!!e.data.refundRequestedAt).length;L.push({type:`banner`,title:s.mismatchTitle(A.items.length,A.hasMore),description:e===A.items.length?s.mismatchAllRefundPending:e>0?s.mismatchSomeRefundPending(e):s.mismatchNoRefundPending,variant:`error`},mr(A.items.map(e=>e.data),`page-mismatches`,void 0,``,s,c,u))}return w.items.length&&L.push({type:`header`,text:s.invoicesHeader(w.items.length,w.hasMore)},{type:`context`,text:s.invoicesIntro},...hr(w.items.map(e=>e.data),s,c,u),{type:`divider`}),M.length&&L.push({type:`header`,text:s.soldHeader},{type:`table`,page_action_id:`page-sold`,columns:[{key:`event`,label:s.columnEvent},...I?[{key:`date`,label:s.columnDate}]:[],{key:`tickets`,label:s.columnTickets},{key:`total`,label:s.columnTotal},{key:`action`,label:` `,format:`element`}],rows:M.slice(0,50).map(({pool:e,label:t,ledger:n})=>({event:t,date:n.occurrenceStart&&ur(n.occurrenceStart,n.timezone,c,s),tickets:Object.entries(n.buckets??{}).map(([t,n])=>`${F.get(`${e}\n${t}`)??t}: ${n.taken}${n.capacity?` / ${n.capacity}`:``}`).join(` · `),total:[n.capacity?s.tierSoldOf(n.taken,n.capacity):s.tierSold(n.taken),n.held?s.heldText(n.held):``].filter(Boolean).join(` · `),action:{type:`button`,action_id:`view-event-orders`,label:s.viewOrders,value:`${e}\n${n.occurrenceStart}`}}))},{type:`divider`}),L.push({type:`header`,text:s.allOrders},{type:`form`,block_id:`orders-filter ${v.pool??``} ${v.occurrence??``} ${v.status??``}`,fields:[{type:`select`,action_id:`pool`,label:s.filterEvent,options:[{label:s.allEvents,value:``},...[...j].slice(0,200).map(([e,t])=>({label:t,value:e}))],initial_value:v.pool??``},{type:`select`,action_id:`occurrence`,label:s.filterDate,condition:{field:`pool`,neq:``},options:[{label:s.allDates,value:``},...N.slice(0,200).map(e=>({label:ur(e,P.get(e),c,s),value:e}))],initial_value:v.occurrence??``},{type:`select`,action_id:`status`,label:s.filterStatus,options:[{label:s.anyStatus,value:``},...tr.map(e=>({label:s.statusLabel[e]??e,value:e}))],initial_value:v.status??``},{type:`text_input`,action_id:`buyer`,label:s.filterBuyer,initial_value:O}],submit:{label:s.showOrders,action_id:`apply-orders-filter`}},{type:`actions`,elements:[...rr(v)?[{type:`button`,action_id:`clear-orders-filter`,label:s.showAllOrders}]:[],{type:`button`,action_id:`prepare-orders-export`,label:s.prepareCsv}]},...y,...O?k.items.length?hr(k.items.map(e=>e.data),s,c,u):[{type:`empty`,title:s.noOrdersForAddress,size:`sm`}]:[mr(k.items.map(e=>e.data),`page-orders`,k.hasMore?k.cursor:void 0,rr(v)?s.noOrdersMatch:s.noOrdersYet,s,c,u),...v.status?[]:[{type:`context`,text:D?s.closedHidden:s.closedShown},{type:`actions`,elements:[{type:`button`,action_id:`toggle-closed-orders`,label:D?s.showClosed:s.hideClosed}]}],{type:`context`,text:s.testHint}],{type:`divider`},{type:`header`,text:s.deleteDataHeader},{type:`context`,text:s.deleteDataIntro},{type:`form`,block_id:`personal-data`,fields:[{type:`text_input`,action_id:`eraseEmail`,label:s.fieldEraseEmail,initial_value:b??``}],submit:{label:s.find,action_id:`find-personal-data`}},...x),{blocks:L,toast:g}}function br(e){let t=String(e??``).trim(),n=/[#?&]code=([0-9a-fA-F]{64})/.exec(t);return(n?n[1]:t).toLowerCase()}async function xr(e,t,n,r,i=Date.now()){let a=br(n.code);if(!/^[0-9a-f]{64}$/.test(a))return{ok:!1,error:`INVALID_CODE`};let o=(await t.call(async()=>e.storage.orders.query({where:{ticketCodeHash:await L(a)},limit:1}))).items[0]?.data;if(!o)return{ok:!0,result:`UNKNOWN_CODE`};let s=o.startsAt||o.occurrenceStart,c={buyerName:o.buyerName,eventTitle:o.eventTitle,occurrenceStart:s,quantity:o.quantity,tickets:o.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),...o.mode===`test`?{test:!0}:{}},l=typeof n.eventId==`string`?n.eventId.trim():``,u=typeof n.occurrenceStart==`string`?n.occurrenceStart.trim():``;if(o.status!==`paid`)return{ok:!0,result:`NOT_VALID`,status:o.status,...c};if(l&&l!==o.eventId&&l!==o.poolKey)return{ok:!0,result:`WRONG_EVENT`,...c};if(u&&s&&Date.parse(u)!==Date.parse(s))return{ok:!0,result:`WRONG_DATE`,...c};if(o.checkedInAt)return{ok:!0,result:`ALREADY_CHECKED_IN`,checkedInAt:o.checkedInAt,...c};let d=new Date(i).toISOString();return(await t.call(()=>e.storage.orders.updateIf(o.id,{where:{status:`paid`,checkedInAt:null},set:{checkedInAt:d,...r?{checkedInBy:r}:{},updatedAt:d}}))).applied?{ok:!0,result:`ADMITTED`,checkedInAt:d,...c}:{ok:!0,result:`ALREADY_CHECKED_IN`,...c}}const Sr=`tickets-hold-sweep`,Cr=`tickets-reconcile`,wr=`tickets-retention`,Tr=`tickets-auto-refund`,Er=[{name:Sr,schedule:`* * * * *`},{name:Cr,schedule:`*/10 * * * *`},{name:wr,schedule:`23 * * * *`},{name:Tr,schedule:`*/5 * * * *`},{name:Ut,schedule:`* * * * *`}];async function Dr(e){if(e)for(let t of Er)await e.schedule(t.name,{schedule:t.schedule})}async function Or(e,t){if(!e||!t.has(1))return[];let n=await t.call(()=>e.list()),r=[];for(let i of Er)if(!n.some(e=>e.name===i.name&&e.schedule===i.schedule)){if(!t.has(1))break;await t.call(()=>e.schedule(i.name,{schedule:i.schedule})),r.push(i.name)}return r}function kr(e,t,n){return{type:e,data:{object:{...n??{},id:n?.id??t.stripeSessionId,metadata:{order_id:t.id},client_reference_id:t.id}}}}async function Ar(e,t,n,r,i){if(!r.stripeSessionId){let n=await On(e,t,kr(`checkout.session.expired`,r),i.nowMs);return n.status===200?n.result:`ERROR`}let o=`/checkout/sessions/${encodeURIComponent(r.stripeSessionId)}`,s=await gn(e.http,t,n,`GET`,o);if(!s.ok)return a(t,e.log,`warn`,`Could not read the Stripe session; trying again later`,{orderId:r.id,error:s.error,status:s.status}),`STRIPE_UNAVAILABLE`;let c=s.data;if(c.status===`open`){if(!i.expireOpen)return`STILL_OPEN`;let r=await gn(e.http,t,n,`POST`,`${o}/expire`,{});if(!r.ok)return`EXPIRE_REFUSED`;c=r.data}let l=c.status===`complete`?`checkout.session.completed`:c.status===`expired`?`checkout.session.expired`:``;if(!l)return`UNKNOWN_STATUS`;let u=await On(e,t,kr(l,r,c),i.nowMs);return u.status===200?u.result:`ERROR`}async function jr(e,t){let n=String(await t.call(()=>e.settings.get(Ce))??``).trim();return[`missing`,`unrecognised`].includes(J(n))||!e.http?null:n}async function Mr(e,t,n=Date.now()){let r={checked:0,outcomes:[]},i=await t.call(()=>e.storage.orders.query({where:{status:`pending`,holdExpiresAt:{lte:new Date(n).toISOString()}},orderBy:{holdExpiresAt:`asc`},limit:2}));if(i.items.length===0)return r;let a=await jr(e,t);for(let{data:o}of i.items){if(!t.has(8))break;if(r.checked++,o.stripeSessionId&&!a){r.outcomes.push({orderId:o.id,result:`STRIPE_NOT_CONFIGURED`});continue}r.outcomes.push({orderId:o.id,result:await Ar(e,t,a??``,o,{expireOpen:!0,nowMs:n})})}return r}function Nr(e,t,n,r=Date.now()){return O(e.kv,t,n,async({poolKey:t,occurrenceStart:n,budget:r,isQuiet:i})=>{let a=0,o=0,s={},c=await E(r,r=>e.storage.orders.query({where:{poolKey:t,occurrenceStart:n},limit:100,cursor:r}),e=>{if(!i(e.updatedAt))return!1;let t=e.status===`paid`&&e.needsAttention!==`PAID_NO_SEATS`&&e.needsAttention!==`AMOUNT_MISMATCH`||e.status===`pending_invoice`,n=e.status===`pending`;if(!(!t&&!n)){for(let n of e.lines){let e=s[n.tierId]??={taken:0,held:0};t?e.taken+=n.quantity:e.held+=n.quantity}t?a+=e.quantity:o+=e.quantity}},1);return c===`OK`?{taken:a,held:o,buckets:s}:c},{nowMs:r,log:e.log})}function Pr(e,t,n=Date.now()){return k(e.kv,t,`state:reconcile-cursor`,(t,r)=>Nr(e,r,t,n))}async function Fr(e,t,n,r=Date.now()){if(typeof n!=`string`||!/^ord_[0-9a-f]{32}$/.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i)return{ok:!1,error:`INVALID_ORDER`};if(i.status!==`pending`||!i.stripeSessionId)return{ok:!0,status:i.status};let a=await jr(e,t);if(!a)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let o=await gn(e.http,t,a,`POST`,`/checkout/sessions/${encodeURIComponent(i.stripeSessionId)}/expire`,{});if(!o.ok||o.data.status!==`expired`)return{ok:!0,status:`pending`};let s=await On(e,t,kr(`checkout.session.expired`,i,o.data),r);return{ok:!0,status:s.status===200&&(s.result===`EXPIRED`||s.result===`ALREADY_DONE`)?`expired`:`pending`}}const Ir=new Set([`PAID`,`PAID_AFTER_EXPIRY`,`PAID_NO_SEATS`,`AMOUNT_MISMATCH`]);async function Lr(e,t,n,r=Date.now(),i){if(typeof n!=`string`||!/^ord_[0-9a-f]{32}$/.test(n))return{ok:!1,error:`INVALID_ORDER`};let a=await t.call(()=>e.storage.orders.get(n));if(!a)return{ok:!1,error:`INVALID_ORDER`};let o=a.status,s=a.needsAttention;if(a.status===`pending`&&a.stripeSessionId){let n=await jr(e,t);if(n){let i=await Ar(e,t,n,a,{expireOpen:!1,nowMs:r});Ir.has(i)&&(o=`paid`),i===`EXPIRED`&&(o=`expired`),(i===`PAID_NO_SEATS`||i===`PAID_AFTER_EXPIRY`||i===`AMOUNT_MISMATCH`)&&(s=i)}}let c=o===`paid`&&s!==`PAID_NO_SEATS`&&s!==`AMOUNT_MISMATCH`,l=``;if(c&&a.returnKeyHash&&a.orderPageUrl&&ee(i)&&t.has(3)&&await L(i)===a.returnKeyHash){let n=await B(e.kv,t);l=n?await En(n,a):``}return{...pn({...a,status:o}),ticketsIssued:c,...l?{orderPageUrl:l}:{}}}async function Rr(e,t,n=Date.now()){let r=await Y(e.kv,t,n),i=new Date(n-r.retentionDays*864e5).toISOString(),a=new Date(n).toISOString(),o=0;if(t.has(1)){let r=await t.call(()=>e.storage.orders.query({where:{retainFrom:{lt:i}},orderBy:{retainFrom:`asc`},limit:8}));for(let i of r.items){if(!t.has(1))break;let r=i.data;if((r.status===`pending`||r.status===`pending_invoice`||r.status===`paid`&&r.needsAttention!==`PAID_NO_SEATS`&&r.needsAttention!==`AMOUNT_MISMATCH`)&&qn(r.startsAt||r.occurrenceStart,n)){await t.call(()=>e.storage.orders.updateIf(r.id,{where:{retainFrom:r.retainFrom},set:{retainFrom:a}}));continue}(await t.call(()=>e.storage.orders.updateIf(r.id,{where:{retainFrom:r.retainFrom},set:{buyerName:``,buyerEmail:``,answers:[],ipHash:``,retainFrom:`~erased`,erasedAt:a}}))).applied&&o++}}return{orders:o}}async function zr(e,t,n=Date.now()){if(!(await Y(e.kv,t,n)).autoRefund||!t.has(1))return{refunded:0};let r=(await t.call(()=>e.storage.orders.query({where:{needsAttention:`PAID_NO_SEATS`,status:`paid`},limit:20}))).items.map(e=>e.data).filter(e=>!e.refundRequestedAt&&e.paymentMethod===`stripe`&&!!e.stripePaymentIntentId).slice(0,2);if(r.length<2&&t.has(1)){let n=await t.call(()=>e.storage.orders.query({where:{needsAttention:`AMOUNT_MISMATCH`,status:`paid`},limit:20}));for(let{data:e}of n.items){if(r.length>=2)break;!e.refundRequestedAt&&e.paymentMethod===`stripe`&&e.stripePaymentIntentId&&r.push(e)}}let i=0;for(let o of r){if(!t.has(5))break;let r=await Kn(e,t,o.id,n);if(!r.ok){a(t,e.log,`warn`,`Automatic refund failed`,{orderId:o.id,error:r.error});continue}if(!r.already&&(i++,o.buyerEmail&&e.email?.send&&t.has(1))){let n=Jt(o.locale);try{let r=await en(e,t,n);await t.call(()=>e.email.send({to:o.buyerEmail,...$t(r.refund,{title:o.eventTitle,date:Q(o,n)})}))}catch(n){a(t,e.log,`warn`,`Automatic refund buyer notice failed`,{orderId:o.id,error:String(n)})}}}return i>0&&a(t,e.log,`info`,`Automatic refunds requested`,{refunded:i}),{refunded:i}}function Br(e,t,n,r){if(e.ok&&e.kind===`exempt`)return{type:`banner`,title:r.devHostTitle,description:r.devHostDescription(e.hostname),variant:`default`};if(e.ok)return{type:`banner`,title:r.licensedTitle(e.hostname,e.licence.updatesUntil),description:r.licensedDescription,variant:`default`};if(e.error===`LICENCE_EXPIRED`)return{type:`banner`,title:r.licenceExpiredTitle,description:r.licenceExpiredDescription(e.updatesUntil),variant:`alert`};let i=He(t)||r.thisSite;return{type:`banner`,title:r.licenceInvalidTitle(i,n),description:r.licenceRequiredDescription(i),variant:`alert`}}function Vr(e,t,n,r){if(e.ok&&e.kind===`exempt`)return r.licenceExempt;if(e.ok)return r.licenceActive(e.hostname,e.licence.updatesUntil);if(e.error===`LICENCE_EXPIRED`)return r.licenceExpired(e.updatesUntil);let i=He(t)||r.thisSite;return r.licenceMissing(i,n)}function Hr(e,t,n=q,r,i=Bt()){let a=r??{ok:!1,error:`LICENCE_INVALID`},o=[{type:`header`,text:i.setupTitle},Br(a,e.siteUrl,!!t.licenceKey,i)];return e.encryption?(o.push(e.ready?{type:`banner`,title:Ee(e.stripeKey)?i.readyLiveTitle:i.readyTestTitle,description:Ee(e.stripeKey)?i.readyLiveDescription:i.readyTestDescription,variant:Ee(e.stripeKey)?`alert`:`default`}:{type:`banner`,title:i.notReadyTitle,description:i.notReadyDescription,variant:`alert`},{type:`fields`,fields:[{label:i.fieldStripeKey,value:i.keyLabels[e.stripeKey]},{label:i.fieldWebhookSecret,value:e.webhookSecret?i.webhookSaved:i.webhookNotSaved},{label:i.fieldEncryption,value:i.encryptionPresent},{label:i.fieldSiteUrl,value:e.siteUrl||i.siteUrlUnset}]},{type:`header`,text:i.connectStripe},...e.webhookUrl?[{type:`context`,text:i.webhookStep1(e.webhookUrl)},{type:`context`,text:i.webhookStep2(Te.join(`, `))},{type:`context`,text:i.webhookStep3}]:[{type:`context`,text:i.webhookNoSiteUrl}],{type:`actions`,elements:[{type:`link`,label:i.openSettings,target:{kind:`plugin-settings`},appearance:`primary`}]},...Ur(t,n,a,e.siteUrl,i)),o):(o.push({type:`banner`,title:i.encryptionTitle,description:i.encryptionDescription,variant:`alert`}),o.push(...Ur(t,n,a,e.siteUrl,i)),o)}function Ur(e,t=q,n={ok:!1,error:`LICENCE_INVALID`},r=``,i=Bt()){return[{type:`header`,text:i.eventsHeader},{type:`context`,text:i.componentsHint(t)},{type:`form`,block_id:`tickets-config`,fields:[{type:`text_input`,action_id:`eventCollection`,label:i.fieldEventCollection,initial_value:e.eventCollection},{type:`text_input`,action_id:`checkoutReturnPath`,label:i.fieldCheckoutReturnPath,initial_value:e.checkoutReturnPath},{type:`text_input`,action_id:`orderPagePath`,label:i.fieldOrderPagePath,initial_value:e.orderPagePath},{type:`number_input`,action_id:`retentionDays`,label:i.fieldRetentionDays,min:30,max:3650,initial_value:e.retentionDays},{type:`text_input`,action_id:`defaultTimezone`,label:i.fieldDefaultTimezone,placeholder:`Europe/Zurich`,initial_value:e.defaultTimezone},{type:`toggle`,action_id:`autoRefund`,label:i.fieldAutoRefund,initial_value:e.autoRefund},{type:`text_input`,action_id:`turnstileSiteKey`,label:i.fieldTurnstileSiteKey,initial_value:e.turnstileSiteKey},{type:`text_input`,action_id:`turnstileSecretKey`,label:i.fieldTurnstileSecretKey,placeholder:e.turnstileSecretKey?i.turnstileSecretSaved:``,initial_value:``}],submit:{label:i.save,action_id:`save-config`}},{type:`context`,text:Qe(e)?i.turnstileOn:i.turnstileOff},{type:`context`,text:i.tiersHint},{type:`actions`,elements:[{type:`link`,label:i.openOrders,target:{kind:`plugin-page`,path:Yn}}]},...Wr(e,n,r,i)]}function Wr(e,t,n,r){let i=He(n)||r.thisSite,a=t.ok&&t.kind===`licensed`,o=[];return e.licenceCode&&(o.push({type:`button`,action_id:`licence-refresh`,label:a?r.refreshKey:r.activateThisSite}),a&&o.push({type:`button`,action_id:`licence-deactivate`,label:r.deactivateSite,style:`danger`,confirm:{title:r.deactivateConfirmTitle,text:r.deactivateConfirmText(i),confirm:r.deactivate,deny:r.cancel}})),[{type:`header`,text:r.licenceHeader},{type:`context`,text:r.licenceIntro(i)},{type:`form`,block_id:`tickets-licence`,fields:[{type:`text_input`,action_id:`licence`,label:r.fieldLicence,placeholder:e.licenceCode?r.licenceCodeSaved(e.licenceCode.slice(-4)):e.licenceKey?r.licenceKeySaved:`EVT-XXXX-XXXX-XXXX-XXXX`,initial_value:``}],submit:{label:r.activate,action_id:`licence-activate`}},{type:`context`,text:Vr(t,n,!!e.licenceKey,r)},...o.length?[{type:`actions`,elements:o}]:[],{type:`context`,text:r.lostCode}]}async function Gr(e,t,n,r,i,a){let o=r.kv,s=t.trim();if(e===`activate`&&s.startsWith(`evt1.`))return{config:await nt(o,i,{licenceKey:s,licenceCode:``}),toast:{message:a.keySaved,type:`success`}};let c=e===`activate`&&s?ke(s):n.licenceCode;if(!c)return{config:n,toast:{message:a.notALicence,type:`error`}};let l=He(r.site?.url);if(!l)return{config:n,toast:{message:a.noSiteUrl,type:`error`}};let u=r.http?(e,t)=>r.http.fetch(e,t):void 0;if(e===`deactivate`){let e=await Me(u,i,`deactivate`,c,l);if(e.ok||e.error===`NOT_ACTIVE`){let t=await nt(o,i,{licenceKey:``});if(!e.ok)return{config:t,toast:{message:Kr(e,l,a),type:`info`}};let n=e.status?.movesLeft;return{config:t,toast:{message:a.deactivated(l,n==null?`none`:String(n)),type:`success`}}}return{config:n,toast:{message:Kr(e,l,a),type:`error`}}}if(Ue(l))return{config:c===n.licenceCode?n:await nt(o,i,{licenceCode:c}),toast:{message:a.activatedExempt(l),type:`info`}};let d=await Me(u,i,`activate`,c,l);if(!d.ok)return{config:n,toast:{message:Kr(d,l,a),type:`error`}};if(!d.key)return{config:await nt(o,i,{licenceCode:c}),toast:{message:a.activatedExempt(l),type:`info`}};let f=await nt(o,i,{licenceCode:c,licenceKey:d.key}),p=d.status;return{config:f,toast:{message:a.activated(l,p?.used??1,p?.seats??0),type:`success`}}}function Kr(e,t,n){let r=n.issuerErrors;switch(e.error){case`HOSTNAME_INVALID`:return r.HOSTNAME_INVALID(t);case`NO_FREE_SITE`:return r.NO_FREE_SITE(e.status?.hostnames.join(`, `)||`?`);default:return r[e.error]}}function qr(e){if(!e||e.turnstileSiteKey===void 0)return{};let t=String(e.turnstileSiteKey??``).trim(),n=String(e.turnstileSecretKey??``).trim();return t?{turnstileSiteKey:t,...n?{turnstileSecretKey:n}:{}}:{turnstileSiteKey:``,turnstileSecretKey:``}}const Jr={hooks:{"plugin:activate":async(e,t)=>{await Dr(t.cron)},cron:async(e,t)=>{if(e.name===Sr){let e=new r,n=await Mr(t,e);n.checked&&a(e,t.log,`info`,`Ticket hold sweep`,n)}else if(e.name===Cr)await Pr(t,new r);else if(e.name===wr){let e=new r,n=await Rr(t,e);n.orders>0&&e.has(1)&&a(e,t.log,`info`,`Tickets retention sweep`,n)}else e.name===Tr?await zr(t,new r):e.name===Ut&&await qt(t,new r)},"content:beforeDelete":async(e,t)=>{let n=new r;if((await Y(t.kv,n)).eventCollection===e.collection&&await Jn(t.storage,n,e.id))return a(n,t.log,`warn`,`Delete blocked: the event has live orders`,{id:e.id}),!1}},routes:{admin:{permission:`plugins:manage`,handler:async(e,t)=>{let n=e.input&&typeof e.input==`object`?e.input:{};if(n.page===Yn||n.page===`widget:recent`)return yr(n,t,e.user,e.ui?.locale);let i=new r,a=Bt(await Gt(t,i,e.ui?.locale),e.ui?.locale),o=t.kv,s,c=await Y(o,i);if(n.type===`page_load`){let e=await Or(t.cron,i);e.length&&(s={message:a.scheduledTasks(e.join(`, `)),type:`info`})}if(n.type===`form_submit`&&n.action_id===`save-config`){let e=typeof n.values?.licenceKey==`string`?n.values.licenceKey.trim():``;c=await nt(o,i,{eventCollection:String(n.values?.eventCollection??``),...n.values?.checkoutReturnPath===void 0?{}:{checkoutReturnPath:String(n.values.checkoutReturnPath)},...n.values?.orderPagePath===void 0?{}:{orderPagePath:String(n.values.orderPagePath)},...n.values?.retentionDays===void 0?{}:{retentionDays:Number(n.values.retentionDays)},...n.values?.defaultTimezone===void 0?{}:{defaultTimezone:String(n.values.defaultTimezone)},...n.values?.autoRefund===void 0?{}:{autoRefund:n.values.autoRefund===!0},...e?{licenceKey:e}:{},...qr(n.values)}),s={message:a.savedEvents(c.eventCollection),type:`success`}}if(n.type===`form_submit`&&n.action_id===`licence-activate`){let e=typeof n.values?.licence==`string`?n.values.licence:``;({config:c,toast:s}=await Gr(`activate`,e,c,t,i,a))}if(n.type===`block_action`&&(n.action_id===`licence-refresh`||n.action_id===`licence-deactivate`)){let e=n.action_id===`licence-refresh`?`refresh`:`deactivate`;({config:c,toast:s}=await Gr(e,``,c,t,i,a))}let l=t.plugin?.id||q,[u,d]=await Promise.all([Oe(t,i,l),Fn(c.licenceKey,t.site?.url)]);return{blocks:Hr(u,c,l,d,a),toast:s}}},"admin/editor/tickets":{permission:`content:edit_any`,handler:async(e,t)=>dn(e,t)},"stripe/webhook":t({public:!0,methods:[`POST`],request:{body:`bytes`,headers:[`stripe-signature`],maxBytes:512*1024},response:`raw`,handler:async(t,n)=>{let r=await An(t.input,t.request.headers[`stripe-signature`],n);return e({status:r.status,headers:{"content-type":`application/json; charset=utf-8`},body:{kind:`text`,value:JSON.stringify(r.status===200?{received:!0,result:r.result}:{error:r.reason})}})}}),"checkout/release":t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Fr(t,new r,e.input?.orderId)}),"orders/download":t({public:!0,methods:[`GET`],request:{body:`none`},response:`raw`,handler:async(t,n)=>{let i=await cr(n,new r,new URL(t.request.url).searchParams.get(`token`));return e(i?{status:200,headers:{"content-type":`text/csv; charset=utf-8`,"content-disposition":`attachment; filename="${i.filename}"`,"cache-control":`no-store`},body:{kind:`text`,value:i.body}}:{status:404,headers:{"content-type":`text/plain; charset=utf-8`},body:{kind:`text`,value:`This download link is invalid, expired, or has already been used.`}})}}),order:t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Gn(t,new r,e.input)}),checkin:t({permission:`content:edit_any`,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>xr(t,new r,e.input??{},e.user?.id)}),"checkout/status":t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Lr(t,new r,e.input?.orderId,Date.now(),e.input?.returnKey)}),checkout:t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Rn(e.input,t,Date.now(),e.requestMeta?.ip??null)}),availability:t({public:!0,methods:[`GET`],request:{body:`none`},cacheControl:`public, max-age=15`,handler:async(e,t)=>{let n=new URL(e.request.url),a=new r;return await P(t.kv,t.cron,new i(a,4),Er).catch(()=>`SKIPPED`),bt(t,a,(n.searchParams.get(`eventId`)??``).trim().slice(0,128),(n.searchParams.get(`occurrence`)??``).trim().slice(0,40))}})}};for(let t of[`availability`,`checkout`,`checkout/status`,`checkout/release`,`order`,`orders/download`]){let r=Jr.routes[t];if(!r?.handler)continue;let i=r.handler;r.handler=async(a,o)=>{try{return await i(a,o)}catch(i){if(!(i instanceof n))try{await o.log?.error?.(`Tickets public route failed`,{route:t,error:String(i).slice(0,500)})}catch{}return r.response===`raw`?e({status:503,headers:{"content-type":`text/plain; charset=utf-8`,"cache-control":`no-store`},body:{kind:`text`,value:`Temporarily unavailable. Please try again.`}}):{ok:!1,error:`TRY_AGAIN`}}}}const Yr=Jr.routes[`stripe/webhook`];if(Yr?.handler){let t=Yr.handler;Yr.handler=async(r,i)=>{try{return await t(r,i)}catch(t){if(!(t instanceof n))try{await i.log?.error?.(`Tickets webhook failed`,{error:String(t).slice(0,500)})}catch{}return e({status:500,headers:{"content-type":`application/json; charset=utf-8`,"cache-control":`no-store`},body:{kind:`text`,value:JSON.stringify({error:`WEBHOOK_FAILED`})}})}}}export{Jr as default,Jr as plugin,Hr as setupBlocks};
40
+ `),u=$t(c.ticket,{name:n.buyerName,title:n.eventTitle,date:Q(n,s),lines:l,total:tn(n.totalMinor,n.currency,s,c.free),code:r,link:o,orderId:n.id,quantity:n.quantity,test:n.mode===`test`});try{return await t.call(()=>e.email.send({to:n.buyerEmail,...u})),`sent`}catch(r){return a(t,e.log,`warn`,`Ticket confirmation email failed`,{orderId:n.id,error:String(r)}),`none`}}async function On(e,t,n,r=Date.now()){let i=typeof n?.type==`string`?n.type:``,o=n?.data?.object??{};if(i===`charge.refunded`)return kn(e,t,o,r);if(!i.startsWith(`checkout.session.`))return{status:200,result:`IGNORED`};let s=String(o.metadata?.order_id??o.client_reference_id??``);if(!/^ord_[0-9a-f]{32}$/.test(s))return{status:200,result:`UNKNOWN_ORDER`};let c=await t.call(()=>e.storage.orders.get(s));if(!c)return{status:200,result:`UNKNOWN_ORDER`};if(c.stripeSessionId&&o.id!==c.stripeSessionId)return a(t,e.log,`warn`,`Stripe event for a different session than the order's`,{orderId:s}),{status:200,result:`SESSION_MISMATCH`};let l=new Date(r).toISOString(),u=m(c.poolKey,c.occurrenceStart);if(i===`checkout.session.completed`&&o.payment_status===`paid`||i===`checkout.session.completed`&&o.payment_status===`no_payment_required`||i===`checkout.session.async_payment_succeeded`){if(!Sn(c,o)){let n=typeof o.payment_intent==`string`?o.payment_intent:void 0,r={status:`paid`,paidAt:l,updatedAt:l,needsAttention:`AMOUNT_MISMATCH`,...n?{stripePaymentIntentId:n}:{}};return c.status===`pending`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:r}))).applied?(await C(e.kv,t,u,wn(c,`held`),1),a(t,e.log,`error`,`Stripe payment amount or currency differs from the order`,{orderId:c.id}),{status:200,result:`AMOUNT_MISMATCH`}):{status:200,result:`ALREADY_DONE`}:c.status===`expired`&&(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`expired`},set:r}))).applied?(a(t,e.log,`error`,`Stripe payment amount or currency differs from the order`,{orderId:c.id}),{status:200,result:`AMOUNT_MISMATCH`}):{status:200,result:`ALREADY_DONE`}}let n=await B(e.kv,t),r=await Cn(n,c),i=typeof o.payment_intent==`string`?o.payment_intent:void 0,s={status:`paid`,paidAt:l,updatedAt:l,ticketCodeHash:await L(r),...i?{stripePaymentIntentId:i}:{}},d=await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:s}));if(d.applied)return await C(e.kv,t,u,wn(c,`heldToTaken`),1),await Dn(e,t,d.data,r,n),{status:200,result:`PAID`};if(c.status===`expired`&&t.has(5)){let i=ht(await t.call(()=>e.storage.tier_settings.get(c.poolKey)),c.poolKey),o=await t.call(()=>e.kv.getVersioned(u)),l=i.capacity??_(o?.value)?.capacity??0,d=await T(e.kv,t,u,{capacity:l,lines:c.lines.map(e=>({bucket:e.tierId,quantity:e.quantity,bucketCapacity:i.tiers.find(t=>t.id===e.tierId)?.capacity??0})),poolKey:c.poolKey,occurrenceStart:c.occurrenceStart,...c.timezone?{timezone:c.timezone}:{},confirm:!0,maxAttempts:1}),f=d.ok?`PAID_AFTER_EXPIRY`:`PAID_NO_SEATS`,p=d.ok?{...s,needsAttention:f}:{...s,ticketCodeHash:void 0,needsAttention:f},m=t.has(1)?await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`expired`},set:p})):{applied:!1};return m.applied?(a(t,e.log,d.ok?`warn`:`error`,`Stripe payment arrived after the seat hold expired`,{orderId:c.id,needsAttention:f}),d.ok&&await Dn(e,t,m.data,r,n),{status:200,result:f}):{status:200,result:`ALREADY_DONE`}}return{status:200,result:`ALREADY_DONE`}}return i===`checkout.session.completed`&&o.payment_status===`unpaid`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:{holdExpiresAt:new Date(r+6048e5).toISOString(),updatedAt:l}})),{status:200,result:`AWAITING_PAYMENT`}):i===`checkout.session.expired`||i===`checkout.session.async_payment_failed`?(await t.call(()=>e.storage.orders.updateIf(c.id,{where:{status:`pending`},set:{status:`expired`,updatedAt:l}}))).applied?(await C(e.kv,t,u,wn(c,`held`),1),{status:200,result:`EXPIRED`}):{status:200,result:`ALREADY_DONE`}:{status:200,result:`IGNORED`}}async function kn(e,t,n,r){let i=typeof n?.payment_intent==`string`?n.payment_intent:``,a=Number(n?.amount_refunded);if(!/^pi_[A-Za-z0-9_]+$/.test(i)||!Number.isSafeInteger(a)||a<=0)return{status:200,result:`IGNORED`};let o=String(n?.metadata?.order_id??``),s=null;if(/^ord_[0-9a-f]{32}$/.test(o)&&(s=await t.call(()=>e.storage.orders.get(o))),s||=(await t.call(()=>e.storage.orders.query({where:{stripePaymentIntentId:i},limit:1}))).items[0]?.data??null,!s||s.paymentMethod!==`stripe`)return{status:200,result:`UNKNOWN_ORDER`};if(s.stripePaymentIntentId&&s.stripePaymentIntentId!==i)return{status:200,result:`SESSION_MISMATCH`};if(s.status===`pending`||s.status===`expired`)return{status:503,reason:`REFUND_BEFORE_PAYMENT`};if(s.status!==`paid`||a<=(s.refundedMinor??0))return{status:200,result:`ALREADY_DONE`};let c=new Date(r).toISOString(),l=n?.refunded===!0||a>=s.totalMinor,u=l?{status:`refunded`,refundedMinor:a,refundedAt:c,updatedAt:c}:{refundedMinor:a,updatedAt:c},d=s;return(await t.call(()=>e.storage.orders.updateIf(d.id,{where:{status:`paid`},set:u}))).applied?l?(d.needsAttention!==`PAID_NO_SEATS`&&await C(e.kv,t,m(d.poolKey,d.occurrenceStart),wn(d,`taken`),1),{status:200,result:`REFUNDED`}):{status:200,result:`PARTIALLY_REFUNDED`}:{status:200,result:`ALREADY_DONE`}}async function An(e,t,n,i=Date.now()){let a=new r,o=String(await a.call(()=>n.settings.get(we))??``).trim();if(!o.startsWith(`whsec_`))return{status:503,reason:`WEBHOOK_NOT_CONFIGURED`};let s=await xn(e,t,o,Math.floor(i/1e3));if(!s.ok)return{status:400,reason:s.reason};let c;try{c=JSON.parse(typeof e==`string`?e:new TextDecoder(`utf-8`,{fatal:!0}).decode(e))}catch{return{status:400,reason:`MALFORMED_JSON`}}return On(n,a,c,i)}async function jn(e,t){return(await I(e,`ip`,[t])).slice(0,32)}async function Mn(e,t,n,r=Date.now()){return(await t.call(()=>e.query({where:{ipHash:n,createdAt:{gt:new Date(r-6e5).toISOString()}},limit:10}))).items.length>=10}const Nn=`2026-11-14`,Pn=Ze({publicKey:`NP1DWBqz-d5wgkCaoKbedn5gB1mmjyi2d-ZF5iJPT1c`,buildDate:Nn});let Fn=null;async function In(e,t,n=Date.now()){let r=Ue(t),i=`${e}\0${r}`;if(Fn&&Fn.cacheKey===i&&n>=Fn.at&&n-Fn.at<6e4)return Fn.result;let a=await Pn({key:e,hostname:r});return Fn={cacheKey:i,result:a,at:n},a}async function Ln(e,t,n,r,i=``){if(!n.buyerEmail||!e.email?.send||!t.has(1))return`none`;let o=Jt(n.locale),s=await en(e,t,o),c=n.lines.map(e=>{let t=e.unitPriceMinor*e.quantity;return` ${e.quantity} × ${e.tierName}${t>0?` ${tn(t,n.currency,o)}`:``}`}).join(`
41
+ `),l=$t(s.invoice,{name:n.buyerName,title:n.eventTitle,date:Q(n,o),lines:c,totalDue:tn(n.totalMinor,n.currency,o,s.free),instructions:r,reference:n.id,link:i});try{return await t.call(()=>e.email.send({to:n.buyerEmail,...l})),`sent`}catch(r){return a(t,e.log,`warn`,`Invoice instructions email failed`,{orderId:n.id,error:String(r)}),`none`}}const $=(e,t={})=>({ok:!1,error:e,...t});function Rn(e){if(!Array.isArray(e)||e.length===0||e.length>20)return null;let t=new Set,n=[];for(let r of e){let e=r??{},i=typeof e.tierId==`string`?e.tierId:``,a=Number(e.quantity);if(!/^[a-z0-9_]{1,48}$/.test(i)||t.has(i)||!Number.isSafeInteger(a)||a<1)return null;t.add(i),n.push({tierId:i,quantity:a})}return n}async function zn(e,t,n=Date.now(),i=null){let a=new r,o=e&&typeof e==`object`?e:{};if(!ce(typeof o.honeypot==`string`?o.honeypot:void 0))return{ok:!0,status:`pending`};if(!le(typeof o.formTimestamp==`number`?o.formTimestamp:void 0,n))return $(`INVALID_SUBMISSION_TIMING`);if(!se(o.commandId))return $(`MISSING_COMMAND_ID`);let s=String(o.eventId??``).trim().slice(0,128),c=String(o.occurrenceStart??``).trim().slice(0,40),d=String(o.buyerName??``).trim().slice(0,100),f=oe(o.buyerEmail),p=Jt(o.locale);if(!s||!d||!o.buyerEmail)return $(`MISSING_REQUIRED_FIELDS`);if(!f)return $(`INVALID_EMAIL`);if(c&&Number.isNaN(Date.parse(c)))return $(`INVALID_OCCURRENCE`);let h=o.paymentMethod===`invoice`,g=ee(o.returnKeyHash)?o.returnKeyHash:``,_=Rn(o.lines);if(!_)return $(`INVALID_LINES`);let v=_.reduce((e,t)=>e+t.quantity,0);if(v>50)return $(`TOO_MANY_SEATS`,{max:50});let y=await J(t.kv,a,n),b=await In(y.licenceKey,t.site?.url,n);if(!b.ok)return $(b.error===`LICENCE_EXPIRED`?`LICENCE_EXPIRED`:`LICENCE_REQUIRED`);let x=await fn(s,o.commandId),S=await a.call(()=>t.storage.orders.get(x));if(S)return mn(S,{eventId:s,occurrenceStart:c,buyerEmail:f,lines:_})?{...pn(S),replayed:!0}:$(`IDEMPOTENCY_CONFLICT`);if($e(y)){if(!t.http)return $(`TRY_AGAIN`);if(!(await Se((e,n)=>t.http.fetch(e,n),a,{secret:y.turnstileSecretKey,token:o.turnstileToken,remoteIp:i})).ok)return $(`CHALLENGE_FAILED`)}let w=await a.call(()=>t.content.get(y.eventCollection,s));if(!w||w.status!==`published`)return $(`EVENT_NOT_FOUND`);if(!ue(w))return $(`EVENT_NOT_TICKETED`);let E=de(w),D=ht(await a.call(()=>t.storage.tier_settings.get(E)),E);if(!D.currency||D.tiers.length===0)return $(`NOT_ON_SALE`);let O=[],k=[];for(let e of _){let t=D.tiers.find(t=>t.id===e.tierId);if(!t)return $(`UNKNOWN_TIER`,{tierId:e.tierId});let r=gt(t,1/0,n);if(r!==`on_sale`)return $(`TIER_NOT_ON_SALE`,{tierId:t.id,status:r});if(e.quantity>t.maxPerOrder)return $(`MAX_PER_ORDER_EXCEEDED`,{tierId:t.id,max:t.maxPerOrder});O.push({tierId:t.id,tierName:t.name,quantity:e.quantity,unitPriceMinor:t.priceMinor}),k.push({bucket:t.id,quantity:e.quantity,bucketCapacity:t.capacity})}let A=O.reduce((e,t)=>e+t.unitPriceMinor*t.quantity,0),j=he(D.questions??[],o.answers);if(!j.ok)return $(`INVALID_ANSWERS`,{fields:j.errors});let M=(h||D.cardOff===!0)&&A>0;if(M&&!D.invoice?.enabled)return $(`INVOICE_NOT_AVAILABLE`);let N=A===0||M,P=String(t.site?.url??``).replace(/\/+$/,``),F=``;if(A>0&&!M){F=String(await a.call(()=>t.settings.get(Ce))??``).trim();let e=Ee(F);if(e===`missing`||e===`unrecognised`||!/^https?:\/\//.test(P)||!t.http)return $(`PAYMENTS_NOT_CONFIGURED`)}let I=A===0?await B(t.kv,a):``,R=I?await Cn(I,{id:x,occurrenceStart:c}):``,z=I,te=typeof i==`string`?i.trim():``,ne=N?3:4,re=N?1:0,ie=``;if(!$e(y)&&te&&(!z&&a.has(ne+re+2)&&(z=await B(t.kv,a)),z&&a.has(ne+re+1)&&(ie=await jn(z,te),await Mn(t.storage.orders,a,ie,n))))return $(`RATE_LIMITED`);let ae=String(w.data?.title??`Event`).slice(0,200),V=l(w.data??{},c),H=u(V?.timezone)??u(w.data?.timezone)??(y.defaultTimezone||void 0),U=m(E,c),W=await T(t.kv,a,U,{capacity:_t(D,w),lines:k,poolKey:E,occurrenceStart:c,label:ae,...H?{timezone:H}:{},confirm:N,maxAttempts:a.has(6)?2:1});if(!W.ok)return W.reason===`FULL`?$(`SOLD_OUT`):W.reason===`BUCKET_FULL`?$(`TIER_SOLD_OUT`,{tierId:W.bucket}):$(`TRY_AGAIN`);let G=new Date(n).toISOString(),fe={id:x,commandId:o.commandId,eventId:s,poolKey:E,occurrenceStart:c,...V?.startsAt?{startsAt:V.startsAt}:{},...V?{allDay:V.allDay}:{},...H?{timezone:H}:{},locale:p,eventTitle:ae,buyerName:d,buyerEmail:f,lines:O,quantity:v,totalMinor:A,currency:D.currency,status:A===0?`paid`:M?`pending_invoice`:`pending`,paymentMethod:A===0?`free`:M?`invoice`:`stripe`,...j.answers.length?{answers:j.answers}:{},...ie?{ipHash:ie}:{},...g&&A>0&&!M?{returnKeyHash:g}:{},.../^https?:\/\//.test(P)?{orderPageUrl:`${P}${y.orderPagePath}`}:{},retainFrom:V?.startsAt||c||G,createdAt:G,...A===0?{paidAt:G}:{},updatedAt:G},pe=async e=>{if(!a.has(2))return;let n=e?`taken`:`held`;await C(t.kv,a,U,{[n]:v,buckets:Object.fromEntries(O.map(e=>[e.tierId,{[n]:e.quantity}]))},1)},K=R?{...fe,ticketCodeHash:await L(R)}:fe;if(A>0&&!M){let e=Q({startsAt:V?.startsAt,occurrenceStart:c,timezone:H,allDay:V?.allDay},p),r=Math.floor(n/1e3)+1860,i=e=>`${P}${y.checkoutReturnPath}?result=${e}&order=${x}`,o=await _n(t.http,a,F,{orderId:x,eventId:s,currency:D.currency,customerEmail:f,successUrl:i(`success`),cancelUrl:i(`cancelled`),expiresAt:r,lines:O.map(t=>({name:`${ae} · ${t.tierName}${e?` · ${e}`:``}`.slice(0,250),unitAmountMinor:t.unitPriceMinor,quantity:t.quantity}))});if(!o.ok||!o.data.url)return await pe(!1),$(`PAYMENT_UNAVAILABLE`);K={...fe,mode:De(Ee(F))?`live`:`test`,stripeSessionId:o.data.id,checkoutUrl:o.data.url,holdExpiresAt:new Date((o.data.expires_at||r)*1e3+3e5).toISOString()}}if(!a.has(1))return $(`TRY_AGAIN`);if(!(await a.call(()=>t.storage.orders.compareAndSet(x,null,K))).applied){await pe(N);let e=a.has(1)?await a.call(()=>t.storage.orders.get(x)):null;return e?{...pn(e),replayed:!0}:$(`TRY_AGAIN`)}let me=R?await Dn(t,a,K,R,I):`none`;if(M){let e=z||(a.has(3)?await B(t.kv,a):``),n=e?await En(e,K):``,r=await Ln(t,a,K,D.invoice.instructions,n);return{...pn(K),instructions:D.invoice.instructions,email:r,...n?{orderPageUrl:n}:{}}}let ge=I?await En(I,K):``;return{...pn(K),...N?{email:me,...ge?{orderPageUrl:ge}:{}}:{}}}const Bn=(e,t)=>new Date(e.getTime()+t*864e5).toISOString().slice(0,10);async function Vn(e,t,n=new Date){let r=await J(e.kv,t),i=e.site?.url,a=Ue(i);if(!r.licenceCode||!r.licenceKey||!a||We(a)||!e.http)return!1;let o=e=>e.ok?e.kind===`licensed`?e.licence.updatesUntil:``:e.error===`LICENCE_EXPIRED`?e.updatesUntil:``,s=o(await In(r.licenceKey,i));if(!s||!(s<Nn||Bn(n,7)>=s&&s>=Bn(n,-30)))return!1;let c=await Ne((t,n)=>e.http.fetch(t,n),t,`activate`,r.licenceCode,a);return!c.ok||!c.key||o(await In(c.key,i))<=s?!1:(await Y(e.kv,t,{licenceKey:c.key}),!0)}const Hn=/^ord_[0-9a-f]{32}$/,Un=/^[0-9a-f]{64}$/;function Wn(e){return{taken:e.quantity,buckets:Object.fromEntries(e.lines.map(e=>[e.tierId,{taken:e.quantity}]))}}function Gn(e,t){let n=e.startsAt||e.occurrenceStart;if(t===void 0||!n)return null;let r=Date.parse(n);return Number.isNaN(r)?null:new Date(r-t*36e5).toISOString()}function Kn(e,t,n){return!t||n>=Date.parse(t)||e.checkedInAt||e.refundRequestedAt?!1:e.status===`pending_invoice`?!0:e.status!==`paid`||e.needsAttention===`PAID_NO_SEATS`?!1:e.paymentMethod===`free`||e.paymentMethod===`stripe`&&!!e.stripePaymentIntentId&&!e.refundedMinor}async function qn(e,t,n,r){let i=e.status===`paid`&&e.needsAttention!==`PAID_NO_SEATS`;return{ok:!0,orderId:e.id,status:e.status,eventTitle:e.eventTitle,occurrenceStart:e.startsAt||e.occurrenceStart,...e.allDay?{allDay:!0}:{},...e.timezone?{timezone:e.timezone}:{},locale:e.locale??`en`,buyerName:e.buyerName,lines:e.lines.map(e=>({tierName:e.tierName,quantity:e.quantity,priceMinor:e.unitPriceMinor*e.quantity})),quantity:e.quantity,totalMinor:e.totalMinor,currency:e.currency,...e.mode?{mode:e.mode}:{},...i?{ticketCode:await Cn(t,e)}:{},...e.checkedInAt?{checkedInAt:e.checkedInAt}:{},...e.refundRequestedAt?{refundRequestedAt:e.refundRequestedAt}:{},...e.refundedMinor?{refundedMinor:e.refundedMinor}:{},canCancel:Kn(e,n,r),...n?{cancelUntil:n}:{}}}async function Jn(e,t,n,r=Date.now()){let i=n&&typeof n==`object`?n:{},o=String(i.orderId??``),s=String(i.token??``).toLowerCase();if(!Hn.test(o)||!Un.test(s))return{ok:!1,error:`INVALID_LINK`};let c=await B(e.kv,t);if(await L(s)!==await L(await Tn(c,o)))return{ok:!1,error:`INVALID_LINK`};let l=await t.call(()=>e.storage.orders.get(o));if(!l)return{ok:!1,error:`INVALID_LINK`};let u=Gn(l,ht(await t.call(()=>e.storage.tier_settings.get(l.poolKey)),l.poolKey).selfCancelHours);if(i.action!==`cancel`)return qn(l,c,u,r);if(!Kn(l,u,r))return{ok:!1,error:`CANNOT_CANCEL`,order:await qn(l,c,u,r)};let d=new Date(r).toISOString();if(l.paymentMethod===`stripe`){let n=String(await t.call(()=>e.settings.get(Ce))??``).trim();if([`missing`,`unrecognised`].includes(Ee(n))||!e.http)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let i=await vn(e.http,t,n,l.stripePaymentIntentId,l.id);if(!i.ok)return a(t,e.log,`warn`,`Buyer refund request failed`,{orderId:l.id,error:i.error,status:i.status}),{ok:!1,error:`REFUND_UNAVAILABLE`};let o=t.has(1)?await t.call(()=>e.storage.orders.updateIf(l.id,{where:{status:`paid`},set:{refundRequestedAt:d,updatedAt:d}})):{applied:!1};return{...await qn(o.applied?o.data:{...l,refundRequestedAt:d},c,u,r),cancelled:!0}}let f=l.status,p=await t.call(()=>e.storage.orders.updateIf(l.id,{where:{status:f},set:{status:`cancelled`,updatedAt:d}}));return p.applied?(await C(e.kv,t,m(l.poolKey,l.occurrenceStart),Wn(l),1),{...await qn(p.data,c,u,r),cancelled:!0}):{ok:!1,error:`CANNOT_CANCEL`}}async function Yn(e,t,n,r=Date.now()){if(!Hn.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(i?.status===`refunded`||i?.refundRequestedAt)return{ok:!0,already:!0};if(!i||i.status!==`paid`||i.paymentMethod!==`stripe`||!i.stripePaymentIntentId)return{ok:!1,error:`NOT_REFUNDABLE`};let a=String(await t.call(()=>e.settings.get(Ce))??``).trim();if([`missing`,`unrecognised`].includes(Ee(a))||!e.http)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let o=await vn(e.http,t,a,i.stripePaymentIntentId,i.id);if(!o.ok)return{ok:!1,error:o.error===`STRIPE`&&o.code===`charge_already_refunded`?`ALREADY_REFUNDED`:`REFUND_UNAVAILABLE`};let s=new Date(r).toISOString();return await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`paid`},set:{refundRequestedAt:s,updatedAt:s}})),{ok:!0,already:!1}}function Xn(e,t=Date.now()){if(!e)return!0;let n=Date.parse(e);return Number.isNaN(n)?!1:n>=t-432e5}async function Zn(e,t,n,r=Date.now()){return(await t.call(()=>e.orders.query({where:{eventId:n,status:`paid`},limit:20}))).items.some(e=>e.data.needsAttention!==`PAID_NO_SEATS`&&e.data.needsAttention!==`AMOUNT_MISMATCH`&&Xn(e.data.startsAt||e.data.occurrenceStart,r))?!0:t.has(1)?(await t.call(()=>e.orders.query({where:{eventId:n,status:`pending`},limit:20}))).items.some(e=>Xn(e.data.startsAt||e.data.occurrenceStart,r))?!0:t.has(1)?(await t.call(()=>e.orders.query({where:{eventId:n,status:`pending_invoice`},limit:20}))).items.some(e=>Xn(e.data.startsAt||e.data.occurrenceStart,r)):!1:!1}const Qn=`/orders`,$n=`export:`,er=/^ord_[0-9a-f]{32}$/;function tr(e,t){return{[t]:e.quantity,buckets:Object.fromEntries(e.lines.map(e=>[e.tierId,{[t]:e.quantity}]))}}async function nr(e,t,n,r=Date.now()){if(!er.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i||i.status!==`pending_invoice`)return{ok:!1,error:`NOT_AWAITING_PAYMENT`};let a=await B(e.kv,t),o=await Cn(a,i),s=new Date(r).toISOString(),c=await L(o),l=await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`pending_invoice`},set:{status:`paid`,paidAt:s,updatedAt:s,ticketCodeHash:c}}));return l.applied?(await Dn(e,t,l.data,o,a),{ok:!0,order:l.data}):{ok:!1,error:`NOT_AWAITING_PAYMENT`}}async function rr(e,t,n,r=Date.now()){if(!er.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i||i.status!==`pending_invoice`)return{ok:!1,error:`NOT_AWAITING_PAYMENT`};let a=new Date(r).toISOString();return(await t.call(()=>e.storage.orders.updateIf(i.id,{where:{status:`pending_invoice`},set:{status:`cancelled`,updatedAt:a}}))).applied?(await C(e.kv,t,m(i.poolKey,i.occurrenceStart),tr(i,`taken`),1),{ok:!0}):{ok:!1,error:`NOT_AWAITING_PAYMENT`}}const ir=[`pending`,`paid`,`pending_invoice`,`expired`,`cancelled`,`refunded`];function ar(e){let t=e&&typeof e==`object`?e:{},n=typeof t.pool==`string`&&/^[A-Za-z0-9_-]{1,128}$/.test(t.pool)?t.pool:void 0,r=n&&typeof t.occurrence==`string`&&!Number.isNaN(Date.parse(t.occurrence))?t.occurrence:void 0,i=ir.includes(t.status)?t.status:void 0;return{...n?{pool:n}:{},...r?{occurrence:r}:{},...i?{status:i}:{},...t.closed===!0?{closed:!0}:{}}}function or(e){let t={};return e.pool&&(t.poolKey=e.pool),e.pool&&e.occurrence&&(t.occurrenceStart=e.occurrence),e.status&&(t.status=e.status),Object.keys(t).length?t:void 0}function sr(e){let t=e==null?``:String(e);return/^[=+\-@\t\r\uFF1D\uFF0B\uFF0D\uFF20]/.test(t)&&(t=`'${t}`),`"${t.replace(/"/g,`""`)}"`}function cr(e){let t=new Uint8Array(e);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,`0`)).join(``)}const lr=`eventual-tickets`;async function ur(e,t,n,r,i=lr){let a=n.replace(/\/+$/,``);if(!/^https?:\/\//.test(a))return null;let o=cr(32),s=new Date(Date.now()+6e5).toISOString();return await t.call(async()=>e.set(`${$n}${await L(o)}`,{...r,expiresAt:s})),{url:`${a}/_emdash/api/plugins/${i||lr}/orders/download?token=${o}`,expiresAt:s}}async function dr(e,t,n){if(!ee(n))return null;let r=`${$n}${await L(n)}`,i=await t.call(()=>e.kv.getVersioned(r));return!i||!(await t.call(()=>e.kv.compareAndDelete(r,i.revision))).applied||!(Date.now()<Date.parse(i.value.expiresAt))?null:fr(e,t,ar(i.value))}async function fr(e,t,n){let r=[],i,a=!1;do{if(!t.has(1)){a=!0;break}let o=await t.call(()=>e.storage.orders.query({where:or(n),orderBy:{createdAt:`desc`},limit:100,cursor:i}));r.push(...o.items.map(e=>e.data)),i=o.hasMore?o.cursor:void 0}while(i);let o=[[`Order`,`Created`,`Status`,`Mode`,`Payment`,`Buyer`,`Email`,`Event`,`Date`,`Local time`,`Tickets`,`Quantity`,`Total`,`Refunded`,`Paid at`,`Needs attention`,`Answers`].map(sr).join(`,`)];a&&o.unshift(sr(`First ${r.length} orders only: filter by event to export the rest.`));for(let e of r)o.push([e.id,e.createdAt,e.status,e.mode??``,e.paymentMethod,e.erasedAt?`—`:e.buyerName,e.erasedAt?`—`:e.buyerEmail,e.eventTitle,e.startsAt||e.occurrenceStart||`Single event`,e.startsAt||e.occurrenceStart?Q(e,`en`):`Single event`,e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`; `),e.quantity,lt(e.totalMinor,e.currency),e.refundedMinor?lt(e.refundedMinor,e.currency):``,e.paidAt??``,e.needsAttention??``,ge(e.answers)].map(sr).join(`,`));return{filename:`orders-${(n.pool??`all`).toLowerCase().replace(/[^a-z0-9_-]+/g,`-`).slice(0,40)}-${Date.now()}.csv`,body:o.join(`\r
42
+ `)}}function pr(e,t,n,r){return e?d(e,t,n):r.singleEvent}function mr(e,t){let n=t.statusLabel[e.status]??e.status,r=e.status===`paid`&&e.refundedMinor?t.partRefunded:``,i=e.status===`paid`&&e.refundRequestedAt&&!e.refundedMinor?t.refundRequested:``,a=e.checkedInAt?t.checkedIn:``;return`${e.mode===`test`?t.testPrefix:``}${n}${r}${i}${a}`}function hr(e,t,n){if(e.status===`paid`&&e.paymentMethod===`stripe`&&e.stripePaymentIntentId&&!e.refundRequestedAt&&!e.refundedMinor)return{type:`button`,action_id:`refund-order`,label:t.refund,style:`danger`,value:e.id,confirm:{title:t.refundTitle(e.buyerName),text:t.refundText(n(e.totalMinor,e.currency)),confirm:t.refund,deny:t.keep}};if(!(e.status!==`pending_invoice`||e.erasedAt))return{type:`button`,action_id:`mark-invoice-paid`,label:t.markPaid,value:e.id,confirm:{title:t.markPaidTitle(e.buyerName),text:t.markPaidText(n(e.totalMinor,e.currency)),confirm:t.markPaidConfirm,deny:t.notYet}}}function gr(e,t){if(!(e.status!==`pending_invoice`||e.erasedAt))return{type:`button`,action_id:`cancel-invoice-order`,label:t.cancel,style:`danger`,value:e.id,confirm:{title:t.cancelTitle(e.buyerName),text:t.cancelText,confirm:t.cancelConfirm,deny:t.keep}}}function _r(e,t,n,r,i,a,o){return{type:`table`,page_action_id:t,...n?{next_cursor:n}:{},columns:[{key:`created`,label:i.columnOrdered},{key:`buyer`,label:i.columnBuyer},{key:`event`,label:i.columnEvent},{key:`date`,label:i.columnDate},{key:`tickets`,label:i.columnTickets},{key:`total`,label:i.columnTotal},{key:`status`,label:i.columnStatus,format:`badge`},{key:`action`,label:i.columnActions,format:`element`},{key:`cancel`,label:` `,format:`element`}],rows:e.map(e=>({created:pr(e.createdAt,e.timezone,a,i),buyer:e.erasedAt?`—`:`${e.buyerName} <${e.buyerEmail}>`,event:e.eventTitle,date:e.startsAt||e.occurrenceStart?Q(e,a):i.singleEvent,tickets:[e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),e.erasedAt?``:ge(e.answers)].filter(Boolean).join(` · `),total:o(e.totalMinor,e.currency),status:e.erasedAt?`${mr(e,i)} · ${i.erased.toLowerCase()}`:mr(e,i),action:hr(e,i,o),cancel:gr(e,i)})),empty_text:r}}function vr(e,t,n,r){return e.flatMap(e=>{let i=[hr(e,t,r),gr(e,t)].filter(e=>!!e);return[{type:`section`,text:[e.erasedAt?`—`:e.buyerName,r(e.totalMinor,e.currency),e.erasedAt?`${mr(e,t)} · ${t.erased.toLowerCase()}`:mr(e,t),e.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),e.eventTitle,e.startsAt||e.occurrenceStart?Q(e,n):``,t.orderedAt(pr(e.createdAt,e.timezone,n,t)),e.erasedAt?``:e.buyerEmail,e.erasedAt?``:ge(e.answers)].filter(Boolean).join(` · `)},...i.length?[{type:`actions`,elements:i}]:[]]})}function yr(e){return{type:`actions`,elements:[{type:`link`,label:e.navSetup,target:{kind:`plugin-page`,path:`/tickets`}},{type:`link`,label:e.navOrders,target:{kind:`plugin-page`,path:Qn}}]}}function br(e){return`state:orders-filter:${e?.id??`shared`}`}function xr(e){if(typeof e==`string`&&e)return e;if(e&&typeof e==`object`&&typeof e.cursor==`string`)return e.cursor}async function Sr(e,t,n,i){let a=new r,o=await Gt(t,a,i),s=Bt(o,i),c=Ot(i),l=Qt(o,c).free,u=(e,t)=>tn(e,t,c,l),d=t,f=e&&typeof e==`object`&&!Array.isArray(e)?e:{},m=String(f.action_id??``),g,v,y=[],b,x=[];if(f.page===`widget:recent`){let e=_r((await a.call(()=>d.storage.orders.query({where:{status:{in:[`paid`,`pending_invoice`]}},orderBy:{createdAt:`desc`},limit:5}))).items.map(e=>e.data),`page-recent`,void 0,s.noOrdersYet,s,c,u);return e.columns=e.columns.filter(e=>/^(buyer|event|total|status)$/.test(e.key)),{blocks:[e,{type:`actions`,elements:[{type:`link`,label:s.allOrders,target:{kind:`plugin-page`,path:Qn}}]}]}}if(f.type===`block_action`&&m===`mark-invoice-paid`&&typeof f.value==`string`){let e=await nr(d,a,f.value);g=e.ok?{message:s.markedPaidToast(e.order.buyerName),type:`success`}:{message:s.noLongerAwaiting,type:`info`}}if(f.type===`block_action`&&m===`cancel-invoice-order`&&typeof f.value==`string`&&(g=(await rr(d,a,f.value)).ok?{message:s.orderCancelled,type:`success`}:{message:s.noLongerAwaiting,type:`info`}),f.type===`block_action`&&m===`refund-order`&&typeof f.value==`string`){let e=await Yn(t,a,f.value);g=e.ok?{message:e.already?s.alreadyRefunded:s.refundRequestedToast,type:`success`}:{message:e.error===`REFUND_UNAVAILABLE`?s.refundUnavailable:s.refundNotPossible,type:`error`}}let S=m===`toggle-closed-orders`;if(f.type===`form_submit`?m===`apply-orders-filter`:f.type===`block_action`&&(S||m===`clear-orders-filter`||m===`view-event-orders`)){let e=ar(await a.call(()=>d.kv.get(br(n)))),[t,r]=String(f.value??``).split(`
43
+ `);v=ar({...S?e:m===`view-event-orders`?{pool:t,occurrence:r}:f.values,closed:S?!e.closed:e.closed}),await a.call(()=>d.kv.set(br(n),v))}if(f.type===`form_submit`&&m===`find-personal-data`){let e=oe(f.values?.eraseEmail);if(e){b=e;let t=await a.call(()=>d.storage.orders.query({where:{buyerEmail:e},limit:20})),n=[],r=!1,i=Date.now();for(let e of t.items){let t=e.data,a=(t.status===`pending`||t.status===`pending_invoice`||t.status===`paid`)&&Xn(t.startsAt||t.occurrenceStart,i);!a&&t.retainFrom!==`~erased`&&(r=!0),n.push({title:t.eventTitle||t.id,date:t.startsAt||t.occurrenceStart?Q(t,c):s.singleEvent,status:a?s.liveNotErased:t.retainFrom===`~erased`?s.erased:s.statusLabel[t.status]??t.status})}x.push({type:`table`,page_action_id:`page-personal-data`,columns:[{key:`title`,label:s.columnEventOrder},{key:`date`,label:s.columnDate},{key:`status`,label:s.columnStatus,format:`badge`}],rows:n,empty_text:s.noOrdersForEmail}),r&&x.push({type:`actions`,elements:[{type:`button`,action_id:`erase-personal-data`,label:s.erasePersonalData,style:`danger`,value:e,confirm:{title:s.eraseTitle,text:s.eraseText,confirm:s.eraseConfirm,deny:s.keep}}]})}}if(f.type===`block_action`&&m===`erase-personal-data`){let e=oe(f.value);if(e){let t=await a.call(()=>d.storage.orders.query({where:{buyerEmail:e},limit:20})),n=Date.now(),r=new Date(n).toISOString(),i=0,o=t.items.map(e=>e.data).filter(e=>e.retainFrom!==`~erased`&&!((e.status===`pending`||e.status===`pending_invoice`||e.status===`paid`)&&Xn(e.startsAt||e.occurrenceStart,n))),c=o.slice(0,6);for(let e of c){if(!a.has(1))break;(await a.call(()=>d.storage.orders.updateIf(e.id,{where:{buyerEmail:e.buyerEmail},set:{buyerName:``,buyerEmail:``,answers:[],ipHash:``,retainFrom:`~erased`,erasedAt:r}}))).applied&&i++}if(g=o.length>i?{message:s.erasedMore(i),type:`info`}:{message:s.erasedAll(i),type:`success`},!a.has(5))return{toast:g,blocks:[yr(s),{type:`header`,text:s.ordersHeader},{type:`context`,text:s.doneReopen}]}}}if(!a.has(5))return{toast:g,blocks:[yr(s),{type:`header`,text:s.ordersHeader},{type:`context`,text:s.doneReopen}]};if(v??=ar(await a.call(()=>d.kv.get(br(n)))),f.type===`block_action`&&m===`prepare-orders-export`){let e=await ur(d.kv,a,String(t.site?.url??``),v,t.plugin?.id);y.push(e?{type:`actions`,elements:[{type:`link`,label:s.downloadOrders,target:{kind:`external`,url:e.url},appearance:`primary`}]}:{type:`banner`,title:s.siteUrlNotSet,description:s.siteUrlNotSetBody,variant:`error`},{type:`context`,text:s.linkWorksOnce})}let C=await a.call(()=>d.storage.orders.query({where:{needsAttention:`PAID_NO_SEATS`,status:`paid`},limit:20})),w=await a.call(()=>d.storage.orders.query({where:{status:`pending_invoice`},orderBy:{createdAt:`asc`},limit:20})),T=await a.call(()=>d.kv.list(p)),E=m===`page-orders`?xr(f.value):void 0,D=!v.status&&!v.closed,O=f.type===`form_submit`&&m===`apply-orders-filter`?oe(f.values?.buyer):``,k=await a.call(()=>d.storage.orders.query(O?{where:{buyerEmail:O},limit:20}:{where:D?{...or(v),status:{in:ir.slice(0,3).concat(`cancelled`)}}:or(v),orderBy:{createdAt:`desc`},limit:50,cursor:E})),A=a.has(1)?await a.call(()=>d.storage.orders.query({where:{needsAttention:`AMOUNT_MISMATCH`,status:`paid`},limit:20})):{items:[],hasMore:!1},j=new Map,M=[],N=[],P=new Map;for(let{key:e,value:t}of T){let n=_(t);if(!n)continue;let r=n.poolKey||h(e,n.occurrenceStart);j.has(r)||j.set(r,n.label||r),(!v.pool||r===v.pool)&&M.push({pool:r,label:n.label||r,ledger:n}),r===v.pool&&n.occurrenceStart&&(N.push(n.occurrenceStart),n.timezone&&P.set(n.occurrenceStart,n.timezone))}N.sort(),M.sort((e,t)=>e.label.localeCompare(t.label)||e.ledger.occurrenceStart.localeCompare(t.ledger.occurrenceStart));let F=new Map;if(M.length&&d.storage.tier_settings&&a.has(1)){let e=await a.call(()=>d.storage.tier_settings.query({...v.pool?{where:{poolKey:v.pool}}:{},limit:100}));for(let{data:t}of e.items)for(let e of t.tiers??[])F.set(`${t.poolKey}\n${e.id}`,e.name)}let I=M.some(e=>e.ledger.occurrenceStart),L=[yr(s),{type:`header`,text:s.ordersHeader}];if(C.items.length){let e=C.items.filter(e=>!!e.data.refundRequestedAt).length;L.push({type:`banner`,title:s.attentionTitle(C.items.length,C.hasMore),description:e===C.items.length?s.attentionAllRefundPending:e>0?s.attentionSomeRefundPending(e):s.attentionNoRefundPending,variant:`error`},_r(C.items.map(e=>e.data),`page-attention`,void 0,``,s,c,u))}if(A.items.length){let e=A.items.filter(e=>!!e.data.refundRequestedAt).length;L.push({type:`banner`,title:s.mismatchTitle(A.items.length,A.hasMore),description:e===A.items.length?s.mismatchAllRefundPending:e>0?s.mismatchSomeRefundPending(e):s.mismatchNoRefundPending,variant:`error`},_r(A.items.map(e=>e.data),`page-mismatches`,void 0,``,s,c,u))}return w.items.length&&L.push({type:`header`,text:s.invoicesHeader(w.items.length,w.hasMore)},{type:`context`,text:s.invoicesIntro},...vr(w.items.map(e=>e.data),s,c,u),{type:`divider`}),M.length&&L.push({type:`header`,text:s.soldHeader},{type:`table`,page_action_id:`page-sold`,columns:[{key:`event`,label:s.columnEvent},...I?[{key:`date`,label:s.columnDate}]:[],{key:`tickets`,label:s.columnTickets},{key:`total`,label:s.columnTotal},{key:`action`,label:` `,format:`element`}],rows:M.slice(0,50).map(({pool:e,label:t,ledger:n})=>({event:t,date:n.occurrenceStart&&pr(n.occurrenceStart,n.timezone,c,s),tickets:Object.entries(n.buckets??{}).map(([t,n])=>`${F.get(`${e}\n${t}`)??t}: ${n.taken}${n.capacity?` / ${n.capacity}`:``}`).join(` · `),total:[n.capacity?s.tierSoldOf(n.taken,n.capacity):s.tierSold(n.taken),n.held?s.heldText(n.held):``].filter(Boolean).join(` · `),action:{type:`button`,action_id:`view-event-orders`,label:s.viewOrders,value:`${e}\n${n.occurrenceStart}`}}))},{type:`divider`}),L.push({type:`header`,text:s.allOrders},{type:`form`,block_id:`orders-filter ${v.pool??``} ${v.occurrence??``} ${v.status??``}`,fields:[{type:`select`,action_id:`pool`,label:s.filterEvent,options:[{label:s.allEvents,value:``},...[...j].slice(0,200).map(([e,t])=>({label:t,value:e}))],initial_value:v.pool??``},{type:`select`,action_id:`occurrence`,label:s.filterDate,condition:{field:`pool`,neq:``},options:[{label:s.allDates,value:``},...N.slice(0,200).map(e=>({label:pr(e,P.get(e),c,s),value:e}))],initial_value:v.occurrence??``},{type:`select`,action_id:`status`,label:s.filterStatus,options:[{label:s.anyStatus,value:``},...ir.map(e=>({label:s.statusLabel[e]??e,value:e}))],initial_value:v.status??``},{type:`text_input`,action_id:`buyer`,label:s.filterBuyer,initial_value:O}],submit:{label:s.showOrders,action_id:`apply-orders-filter`}},{type:`actions`,elements:[...or(v)?[{type:`button`,action_id:`clear-orders-filter`,label:s.showAllOrders}]:[],{type:`button`,action_id:`prepare-orders-export`,label:s.prepareCsv}]},...y,...O?k.items.length?vr(k.items.map(e=>e.data),s,c,u):[{type:`empty`,title:s.noOrdersForAddress,size:`sm`}]:[_r(k.items.map(e=>e.data),`page-orders`,k.hasMore?k.cursor:void 0,or(v)?s.noOrdersMatch:s.noOrdersYet,s,c,u),...v.status?[]:[{type:`context`,text:D?s.closedHidden:s.closedShown},{type:`actions`,elements:[{type:`button`,action_id:`toggle-closed-orders`,label:D?s.showClosed:s.hideClosed}]}],{type:`context`,text:s.testHint}],{type:`divider`},{type:`header`,text:s.deleteDataHeader},{type:`context`,text:s.deleteDataIntro},{type:`form`,block_id:`personal-data`,fields:[{type:`text_input`,action_id:`eraseEmail`,label:s.fieldEraseEmail,initial_value:b??``}],submit:{label:s.find,action_id:`find-personal-data`}},...x),{blocks:L,toast:g}}function Cr(e){let t=String(e??``).trim(),n=/[#?&]code=([0-9a-fA-F]{64})/.exec(t);return(n?n[1]:t).toLowerCase()}async function wr(e,t,n,r,i=Date.now()){let a=Cr(n.code);if(!/^[0-9a-f]{64}$/.test(a))return{ok:!1,error:`INVALID_CODE`};let o=(await t.call(async()=>e.storage.orders.query({where:{ticketCodeHash:await L(a)},limit:1}))).items[0]?.data;if(!o)return{ok:!0,result:`UNKNOWN_CODE`};let s=o.startsAt||o.occurrenceStart,c={buyerName:o.buyerName,eventTitle:o.eventTitle,occurrenceStart:s,quantity:o.quantity,tickets:o.lines.map(e=>`${e.quantity} × ${e.tierName}`).join(`, `),...o.mode===`test`?{test:!0}:{}},l=typeof n.eventId==`string`?n.eventId.trim():``,u=typeof n.occurrenceStart==`string`?n.occurrenceStart.trim():``;if(o.status!==`paid`)return{ok:!0,result:`NOT_VALID`,status:o.status,...c};if(l&&l!==o.eventId&&l!==o.poolKey)return{ok:!0,result:`WRONG_EVENT`,...c};if(u&&s&&Date.parse(u)!==Date.parse(s))return{ok:!0,result:`WRONG_DATE`,...c};if(o.checkedInAt)return{ok:!0,result:`ALREADY_CHECKED_IN`,checkedInAt:o.checkedInAt,...c};let d=new Date(i).toISOString();return(await t.call(()=>e.storage.orders.updateIf(o.id,{where:{status:`paid`,checkedInAt:null},set:{checkedInAt:d,...r?{checkedInBy:r}:{},updatedAt:d}}))).applied?{ok:!0,result:`ADMITTED`,checkedInAt:d,...c}:{ok:!0,result:`ALREADY_CHECKED_IN`,...c}}const Tr=`tickets-hold-sweep`,Er=`tickets-reconcile`,Dr=`tickets-retention`,Or=`tickets-auto-refund`,kr=[{name:Tr,schedule:`* * * * *`},{name:Er,schedule:`*/10 * * * *`},{name:Dr,schedule:`23 * * * *`},{name:Or,schedule:`*/5 * * * *`},{name:Ut,schedule:`* * * * *`}];async function Ar(e){if(e)for(let t of kr)await e.schedule(t.name,{schedule:t.schedule})}async function jr(e,t){if(!e||!t.has(1))return[];let n=await t.call(()=>e.list()),r=[];for(let i of kr)if(!n.some(e=>e.name===i.name&&e.schedule===i.schedule)){if(!t.has(1))break;await t.call(()=>e.schedule(i.name,{schedule:i.schedule})),r.push(i.name)}return r}function Mr(e,t,n){return{type:e,data:{object:{...n??{},id:n?.id??t.stripeSessionId,metadata:{order_id:t.id},client_reference_id:t.id}}}}async function Nr(e,t,n,r,i){if(!r.stripeSessionId){let n=await On(e,t,Mr(`checkout.session.expired`,r),i.nowMs);return n.status===200?n.result:`ERROR`}let o=`/checkout/sessions/${encodeURIComponent(r.stripeSessionId)}`,s=await gn(e.http,t,n,`GET`,o);if(!s.ok)return a(t,e.log,`warn`,`Could not read the Stripe session; trying again later`,{orderId:r.id,error:s.error,status:s.status}),`STRIPE_UNAVAILABLE`;let c=s.data;if(c.status===`open`){if(!i.expireOpen)return`STILL_OPEN`;let r=await gn(e.http,t,n,`POST`,`${o}/expire`,{});if(!r.ok)return`EXPIRE_REFUSED`;c=r.data}let l=c.status===`complete`?`checkout.session.completed`:c.status===`expired`?`checkout.session.expired`:``;if(!l)return`UNKNOWN_STATUS`;let u=await On(e,t,Mr(l,r,c),i.nowMs);return u.status===200?u.result:`ERROR`}async function Pr(e,t){let n=String(await t.call(()=>e.settings.get(Ce))??``).trim();return[`missing`,`unrecognised`].includes(Ee(n))||!e.http?null:n}async function Fr(e,t,n=Date.now()){let r={checked:0,outcomes:[]},i=await t.call(()=>e.storage.orders.query({where:{status:`pending`,holdExpiresAt:{lte:new Date(n).toISOString()}},orderBy:{holdExpiresAt:`asc`},limit:2}));if(i.items.length===0)return r;let a=await Pr(e,t);for(let{data:o}of i.items){if(!t.has(8))break;if(r.checked++,o.stripeSessionId&&!a){r.outcomes.push({orderId:o.id,result:`STRIPE_NOT_CONFIGURED`});continue}r.outcomes.push({orderId:o.id,result:await Nr(e,t,a??``,o,{expireOpen:!0,nowMs:n})})}return r}function Ir(e,t,n,r=Date.now()){return O(e.kv,t,n,async({poolKey:t,occurrenceStart:n,budget:r,isQuiet:i})=>{let a=0,o=0,s={},c=await E(r,r=>e.storage.orders.query({where:{poolKey:t,occurrenceStart:n},limit:100,cursor:r}),e=>{if(!i(e.updatedAt))return!1;let t=e.status===`paid`&&e.needsAttention!==`PAID_NO_SEATS`&&e.needsAttention!==`AMOUNT_MISMATCH`||e.status===`pending_invoice`,n=e.status===`pending`;if(!(!t&&!n)){for(let n of e.lines){let e=s[n.tierId]??={taken:0,held:0};t?e.taken+=n.quantity:e.held+=n.quantity}t?a+=e.quantity:o+=e.quantity}},1);return c===`OK`?{taken:a,held:o,buckets:s}:c},{nowMs:r,log:e.log})}function Lr(e,t,n=Date.now()){return k(e.kv,t,`state:reconcile-cursor`,(t,r)=>Ir(e,r,t,n))}async function Rr(e,t,n,r=Date.now()){if(typeof n!=`string`||!/^ord_[0-9a-f]{32}$/.test(n))return{ok:!1,error:`INVALID_ORDER`};let i=await t.call(()=>e.storage.orders.get(n));if(!i)return{ok:!1,error:`INVALID_ORDER`};if(i.status!==`pending`||!i.stripeSessionId)return{ok:!0,status:i.status};let a=await Pr(e,t);if(!a)return{ok:!1,error:`PAYMENTS_NOT_CONFIGURED`};let o=await gn(e.http,t,a,`POST`,`/checkout/sessions/${encodeURIComponent(i.stripeSessionId)}/expire`,{});if(!o.ok||o.data.status!==`expired`)return{ok:!0,status:`pending`};let s=await On(e,t,Mr(`checkout.session.expired`,i,o.data),r);return{ok:!0,status:s.status===200&&(s.result===`EXPIRED`||s.result===`ALREADY_DONE`)?`expired`:`pending`}}const zr=new Set([`PAID`,`PAID_AFTER_EXPIRY`,`PAID_NO_SEATS`,`AMOUNT_MISMATCH`]);async function Br(e,t,n,r=Date.now(),i){if(typeof n!=`string`||!/^ord_[0-9a-f]{32}$/.test(n))return{ok:!1,error:`INVALID_ORDER`};let a=await t.call(()=>e.storage.orders.get(n));if(!a)return{ok:!1,error:`INVALID_ORDER`};let o=a.status,s=a.needsAttention;if(a.status===`pending`&&a.stripeSessionId){let n=await Pr(e,t);if(n){let i=await Nr(e,t,n,a,{expireOpen:!1,nowMs:r});zr.has(i)&&(o=`paid`),i===`EXPIRED`&&(o=`expired`),(i===`PAID_NO_SEATS`||i===`PAID_AFTER_EXPIRY`||i===`AMOUNT_MISMATCH`)&&(s=i)}}let c=o===`paid`&&s!==`PAID_NO_SEATS`&&s!==`AMOUNT_MISMATCH`,l=``;if(c&&a.returnKeyHash&&a.orderPageUrl&&ee(i)&&t.has(3)&&await L(i)===a.returnKeyHash){let n=await B(e.kv,t);l=n?await En(n,a):``}return{...pn({...a,status:o}),ticketsIssued:c,...l?{orderPageUrl:l}:{}}}async function Vr(e,t,n=Date.now()){let r=await J(e.kv,t,n),i=new Date(n-r.retentionDays*864e5).toISOString(),a=new Date(n).toISOString(),o=0;if(t.has(1)){let r=await t.call(()=>e.storage.orders.query({where:{retainFrom:{lt:i}},orderBy:{retainFrom:`asc`},limit:8}));for(let i of r.items){if(!t.has(1))break;let r=i.data;if((r.status===`pending`||r.status===`pending_invoice`||r.status===`paid`&&r.needsAttention!==`PAID_NO_SEATS`&&r.needsAttention!==`AMOUNT_MISMATCH`)&&Xn(r.startsAt||r.occurrenceStart,n)){await t.call(()=>e.storage.orders.updateIf(r.id,{where:{retainFrom:r.retainFrom},set:{retainFrom:a}}));continue}(await t.call(()=>e.storage.orders.updateIf(r.id,{where:{retainFrom:r.retainFrom},set:{buyerName:``,buyerEmail:``,answers:[],ipHash:``,retainFrom:`~erased`,erasedAt:a}}))).applied&&o++}}return{orders:o}}async function Hr(e,t,n=Date.now()){if(!(await J(e.kv,t,n)).autoRefund||!t.has(1))return{refunded:0};let r=(await t.call(()=>e.storage.orders.query({where:{needsAttention:`PAID_NO_SEATS`,status:`paid`},limit:20}))).items.map(e=>e.data).filter(e=>!e.refundRequestedAt&&e.paymentMethod===`stripe`&&!!e.stripePaymentIntentId).slice(0,2);if(r.length<2&&t.has(1)){let n=await t.call(()=>e.storage.orders.query({where:{needsAttention:`AMOUNT_MISMATCH`,status:`paid`},limit:20}));for(let{data:e}of n.items){if(r.length>=2)break;!e.refundRequestedAt&&e.paymentMethod===`stripe`&&e.stripePaymentIntentId&&r.push(e)}}let i=0;for(let o of r){if(!t.has(5))break;let r=await Yn(e,t,o.id,n);if(!r.ok){a(t,e.log,`warn`,`Automatic refund failed`,{orderId:o.id,error:r.error});continue}if(!r.already&&(i++,o.buyerEmail&&e.email?.send&&t.has(1))){let n=Jt(o.locale);try{let r=await en(e,t,n);await t.call(()=>e.email.send({to:o.buyerEmail,...$t(r.refund,{title:o.eventTitle,date:Q(o,n)})}))}catch(n){a(t,e.log,`warn`,`Automatic refund buyer notice failed`,{orderId:o.id,error:String(n)})}}}return i>0&&a(t,e.log,`info`,`Automatic refunds requested`,{refunded:i}),{refunded:i}}function Ur(e,t,n,r){if(e.ok&&e.kind===`exempt`)return{type:`banner`,title:r.devHostTitle,description:r.devHostDescription(e.hostname),variant:`default`};if(e.ok)return{type:`banner`,title:r.licensedTitle(e.hostname,e.licence.updatesUntil),description:r.licensedDescription,variant:`default`};if(e.error===`LICENCE_EXPIRED`)return{type:`banner`,title:r.licenceExpiredTitle,description:r.licenceExpiredDescription(e.updatesUntil),variant:`alert`};let i=Ue(t)||r.thisSite;return{type:`banner`,title:r.licenceInvalidTitle(i,n),description:r.licenceRequiredDescription(i),variant:`alert`}}function Wr(e,t,n,r){if(e.ok&&e.kind===`exempt`)return r.licenceExempt;if(e.ok)return r.licenceActive(e.hostname,e.licence.updatesUntil);if(e.error===`LICENCE_EXPIRED`)return r.licenceExpired(e.updatesUntil);let i=Ue(t)||r.thisSite;return r.licenceMissing(i,n)}function Gr(e,t,n=q,r,i=Bt()){let a=r??{ok:!1,error:`LICENCE_INVALID`},o=[{type:`header`,text:i.setupTitle},Ur(a,e.siteUrl,!!t.licenceKey,i)];return e.encryption?(o.push(e.ready?{type:`banner`,title:De(e.stripeKey)?i.readyLiveTitle:i.readyTestTitle,description:De(e.stripeKey)?i.readyLiveDescription:i.readyTestDescription,variant:De(e.stripeKey)?`alert`:`default`}:{type:`banner`,title:i.notReadyTitle,description:i.notReadyDescription,variant:`alert`},{type:`fields`,fields:[{label:i.fieldStripeKey,value:i.keyLabels[e.stripeKey]},{label:i.fieldWebhookSecret,value:e.webhookSecret?i.webhookSaved:i.webhookNotSaved},{label:i.fieldEncryption,value:i.encryptionPresent},{label:i.fieldSiteUrl,value:e.siteUrl||i.siteUrlUnset}]},{type:`header`,text:i.connectStripe},...e.webhookUrl?[{type:`context`,text:i.webhookStep1(e.webhookUrl)},{type:`context`,text:i.webhookStep2(Te.join(`, `))},{type:`context`,text:i.webhookStep3}]:[{type:`context`,text:i.webhookNoSiteUrl}],{type:`actions`,elements:[{type:`link`,label:i.openSettings,target:{kind:`plugin-settings`},appearance:`primary`}]},...Kr(t,n,a,e.siteUrl,i)),o):(o.push({type:`banner`,title:i.encryptionTitle,description:i.encryptionDescription,variant:`alert`}),o.push(...Kr(t,n,a,e.siteUrl,i)),o)}function Kr(e,t=q,n={ok:!1,error:`LICENCE_INVALID`},r=``,i=Bt()){return[{type:`header`,text:i.eventsHeader},{type:`context`,text:i.componentsHint(t)},{type:`form`,block_id:`tickets-config`,fields:[{type:`text_input`,action_id:`eventCollection`,label:i.fieldEventCollection,initial_value:e.eventCollection},{type:`text_input`,action_id:`checkoutReturnPath`,label:i.fieldCheckoutReturnPath,initial_value:e.checkoutReturnPath},{type:`text_input`,action_id:`orderPagePath`,label:i.fieldOrderPagePath,initial_value:e.orderPagePath},{type:`number_input`,action_id:`retentionDays`,label:i.fieldRetentionDays,min:30,max:3650,initial_value:e.retentionDays},{type:`text_input`,action_id:`defaultTimezone`,label:i.fieldDefaultTimezone,placeholder:`Europe/Zurich`,initial_value:e.defaultTimezone},{type:`toggle`,action_id:`autoRefund`,label:i.fieldAutoRefund,initial_value:e.autoRefund},{type:`text_input`,action_id:`turnstileSiteKey`,label:i.fieldTurnstileSiteKey,initial_value:e.turnstileSiteKey},{type:`text_input`,action_id:`turnstileSecretKey`,label:i.fieldTurnstileSecretKey,placeholder:e.turnstileSecretKey?i.turnstileSecretSaved:``,initial_value:``}],submit:{label:i.save,action_id:`save-config`}},{type:`context`,text:$e(e)?i.turnstileOn:i.turnstileOff},{type:`context`,text:i.tiersHint},{type:`actions`,elements:[{type:`link`,label:i.openOrders,target:{kind:`plugin-page`,path:Qn}}]},...qr(e,n,r,i)]}function qr(e,t,n,r){let i=Ue(n)||r.thisSite,a=t.ok&&t.kind===`licensed`,o=[];return e.licenceCode&&(o.push({type:`button`,action_id:`licence-refresh`,label:a?r.refreshKey:r.activateThisSite}),a&&o.push({type:`button`,action_id:`licence-deactivate`,label:r.deactivateSite,style:`danger`,confirm:{title:r.deactivateConfirmTitle,text:r.deactivateConfirmText(i),confirm:r.deactivate,deny:r.cancel}})),[{type:`header`,text:r.licenceHeader},{type:`context`,text:r.licenceIntro(i)},{type:`form`,block_id:`tickets-licence`,fields:[{type:`text_input`,action_id:`licence`,label:r.fieldLicence,placeholder:e.licenceCode?r.licenceCodeSaved(e.licenceCode.slice(-4)):e.licenceKey?r.licenceKeySaved:`EVT-XXXX-XXXX-XXXX-XXXX`,initial_value:``}],submit:{label:r.activate,action_id:`licence-activate`}},{type:`context`,text:Wr(t,n,!!e.licenceKey,r)},...o.length?[{type:`actions`,elements:o}]:[],{type:`context`,text:r.lostCode}]}async function Jr(e,t,n,r,i,a){let o=r.kv,s=t.trim();if(e===`activate`&&s.startsWith(`evt1.`))return{config:await Y(o,i,{licenceKey:s,licenceCode:``}),toast:{message:a.keySaved,type:`success`}};let c=e===`activate`&&s?Ae(s):n.licenceCode;if(!c)return{config:n,toast:{message:a.notALicence,type:`error`}};let l=Ue(r.site?.url);if(!l)return{config:n,toast:{message:a.noSiteUrl,type:`error`}};let u=r.http?(e,t)=>r.http.fetch(e,t):void 0;if(e===`deactivate`){let e=await Ne(u,i,`deactivate`,c,l);if(e.ok||e.error===`NOT_ACTIVE`){let t=await Y(o,i,{licenceKey:``});if(!e.ok)return{config:t,toast:{message:Yr(e,l,a),type:`info`}};let n=e.status?.movesLeft;return{config:t,toast:{message:a.deactivated(l,n==null?`none`:String(n)),type:`success`}}}return{config:n,toast:{message:Yr(e,l,a),type:`error`}}}if(We(l))return{config:c===n.licenceCode?n:await Y(o,i,{licenceCode:c}),toast:{message:a.activatedExempt(l),type:`info`}};let d=await Ne(u,i,`activate`,c,l);if(!d.ok)return{config:n,toast:{message:Yr(d,l,a),type:`error`}};if(!d.key)return{config:await Y(o,i,{licenceCode:c}),toast:{message:a.activatedExempt(l),type:`info`}};let f=await Y(o,i,{licenceCode:c,licenceKey:d.key}),p=d.status;return{config:f,toast:{message:a.activated(l,p?.used??1,p?.seats??0),type:`success`}}}function Yr(e,t,n){let r=n.issuerErrors;switch(e.error){case`HOSTNAME_INVALID`:return r.HOSTNAME_INVALID(t);case`NO_FREE_SITE`:return r.NO_FREE_SITE(e.status?.hostnames.join(`, `)||`?`);default:return r[e.error]}}function Xr(e){if(!e||e.turnstileSiteKey===void 0)return{};let t=String(e.turnstileSiteKey??``).trim(),n=String(e.turnstileSecretKey??``).trim();return t?{turnstileSiteKey:t,...n?{turnstileSecretKey:n}:{}}:{turnstileSiteKey:``,turnstileSecretKey:``}}const Zr={hooks:{"plugin:activate":async(e,t)=>{await Ar(t.cron)},cron:async(e,t)=>{if(e.name===Tr){let e=new r,n=await Fr(t,e);n.checked&&a(e,t.log,`info`,`Ticket hold sweep`,n)}else if(e.name===Er)await Lr(t,new r);else if(e.name===Dr&&new Date().getUTCHours()===5)await Vn(t,new r);else if(e.name===Dr){let e=new r,n=await Vr(t,e);n.orders>0&&e.has(1)&&a(e,t.log,`info`,`Tickets retention sweep`,n)}else e.name===Or?await Hr(t,new r):e.name===Ut&&await qt(t,new r)},"content:beforeDelete":async(e,t)=>{let n=new r;if((await J(t.kv,n)).eventCollection===e.collection&&await Zn(t.storage,n,e.id))return a(n,t.log,`warn`,`Delete blocked: the event has live orders`,{id:e.id}),!1}},routes:{admin:{permission:`plugins:manage`,handler:async(e,t)=>{let n=e.input&&typeof e.input==`object`?e.input:{};if(n.page===Qn||n.page===`widget:recent`)return Sr(n,t,e.user,e.ui?.locale);let i=new r,a=Bt(await Gt(t,i,e.ui?.locale),e.ui?.locale),o=t.kv,s,c=await J(o,i);if(n.type===`page_load`){let e=await jr(t.cron,i);e.length&&(s={message:a.scheduledTasks(e.join(`, `)),type:`info`})}if(n.type===`form_submit`&&n.action_id===`save-config`){let e=typeof n.values?.licenceKey==`string`?n.values.licenceKey.trim():``;c=await Y(o,i,{eventCollection:String(n.values?.eventCollection??``),...n.values?.checkoutReturnPath===void 0?{}:{checkoutReturnPath:String(n.values.checkoutReturnPath)},...n.values?.orderPagePath===void 0?{}:{orderPagePath:String(n.values.orderPagePath)},...n.values?.retentionDays===void 0?{}:{retentionDays:Number(n.values.retentionDays)},...n.values?.defaultTimezone===void 0?{}:{defaultTimezone:String(n.values.defaultTimezone)},...n.values?.autoRefund===void 0?{}:{autoRefund:n.values.autoRefund===!0},...e?{licenceKey:e}:{},...Xr(n.values)}),s={message:a.savedEvents(c.eventCollection),type:`success`}}if(n.type===`form_submit`&&n.action_id===`licence-activate`){let e=typeof n.values?.licence==`string`?n.values.licence:``;({config:c,toast:s}=await Jr(`activate`,e,c,t,i,a))}if(n.type===`block_action`&&(n.action_id===`licence-refresh`||n.action_id===`licence-deactivate`)){let e=n.action_id===`licence-refresh`?`refresh`:`deactivate`;({config:c,toast:s}=await Jr(e,``,c,t,i,a))}let l=t.plugin?.id||q,[u,d]=await Promise.all([ke(t,i,l),In(c.licenceKey,t.site?.url)]);return{blocks:Gr(u,c,l,d,a),toast:s}}},"admin/editor/tickets":{permission:`content:edit_any`,handler:async(e,t)=>dn(e,t)},"stripe/webhook":t({public:!0,methods:[`POST`],request:{body:`bytes`,headers:[`stripe-signature`],maxBytes:512*1024},response:`raw`,handler:async(t,n)=>{let r=await An(t.input,t.request.headers[`stripe-signature`],n);return e({status:r.status,headers:{"content-type":`application/json; charset=utf-8`},body:{kind:`text`,value:JSON.stringify(r.status===200?{received:!0,result:r.result}:{error:r.reason})}})}}),"checkout/release":t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Rr(t,new r,e.input?.orderId)}),"orders/download":t({public:!0,methods:[`GET`],request:{body:`none`},response:`raw`,handler:async(t,n)=>{let i=await dr(n,new r,new URL(t.request.url).searchParams.get(`token`));return e(i?{status:200,headers:{"content-type":`text/csv; charset=utf-8`,"content-disposition":`attachment; filename="${i.filename}"`,"cache-control":`no-store`},body:{kind:`text`,value:i.body}}:{status:404,headers:{"content-type":`text/plain; charset=utf-8`},body:{kind:`text`,value:`This download link is invalid, expired, or has already been used.`}})}}),order:t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Jn(t,new r,e.input)}),checkin:t({permission:`content:edit_any`,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>wr(t,new r,e.input??{},e.user?.id)}),"checkout/status":t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>Br(t,new r,e.input?.orderId,Date.now(),e.input?.returnKey)}),checkout:t({public:!0,methods:[`POST`],request:{body:`json`},handler:async(e,t)=>zn(e.input,t,Date.now(),e.requestMeta?.ip??null)}),availability:t({public:!0,methods:[`GET`],request:{body:`none`},cacheControl:`public, max-age=15`,handler:async(e,t)=>{let n=new URL(e.request.url),a=new r;return await P(t.kv,t.cron,new i(a,4),kr).catch(()=>`SKIPPED`),bt(t,a,(n.searchParams.get(`eventId`)??``).trim().slice(0,128),(n.searchParams.get(`occurrence`)??``).trim().slice(0,40))}})}};for(let t of[`availability`,`checkout`,`checkout/status`,`checkout/release`,`order`,`orders/download`]){let r=Zr.routes[t];if(!r?.handler)continue;let i=r.handler;r.handler=async(a,o)=>{try{return await i(a,o)}catch(i){if(!(i instanceof n))try{await o.log?.error?.(`Tickets public route failed`,{route:t,error:String(i).slice(0,500)})}catch{}return r.response===`raw`?e({status:503,headers:{"content-type":`text/plain; charset=utf-8`,"cache-control":`no-store`},body:{kind:`text`,value:`Temporarily unavailable. Please try again.`}}):{ok:!1,error:`TRY_AGAIN`}}}}const Qr=Zr.routes[`stripe/webhook`];if(Qr?.handler){let t=Qr.handler;Qr.handler=async(r,i)=>{try{return await t(r,i)}catch(t){if(!(t instanceof n))try{await i.log?.error?.(`Tickets webhook failed`,{error:String(t).slice(0,500)})}catch{}return e({status:500,headers:{"content-type":`application/json; charset=utf-8`,"cache-control":`no-store`},body:{kind:`text`,value:JSON.stringify({error:`WEBHOOK_FAILED`})}})}}}export{Zr as default,Zr as plugin,Gr as setupBlocks};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vermeulen.solutions/eventual-tickets",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Paid tickets for Eventual RSVP: priced tiers, Stripe Checkout, invoices and signed ticket codes, as a sandboxed EmDash plugin.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",