@frigear-nu/events 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +45 -7
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +53 -42
  5. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
  7. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
  9. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
  11. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
  13. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
  15. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
  16. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
  17. package/dist/runtime/app/components/EventCard.vue +39 -27
  18. package/dist/runtime/app/components/EventTicketCard.vue +5 -4
  19. package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
  20. package/dist/runtime/app/composables/index.d.ts +1 -1
  21. package/dist/runtime/app/composables/index.js +1 -1
  22. package/dist/runtime/app/composables/useEvents.d.ts +10 -4
  23. package/dist/runtime/app/composables/useEvents.js +21 -10
  24. package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin-event.vue +151 -0
  26. package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin.vue +154 -0
  29. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  30. package/dist/runtime/app/pages/event.d.vue.ts +14 -1
  31. package/dist/runtime/app/pages/event.vue +160 -45
  32. package/dist/runtime/app/pages/event.vue.d.ts +14 -1
  33. package/dist/runtime/app/pages/events.vue +12 -10
  34. package/dist/runtime/app/pages/tickets.vue +26 -19
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
  36. package/dist/runtime/app/tabs/Attendees.vue +88 -0
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
  39. package/dist/runtime/app/tabs/Details.vue +74 -0
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
  42. package/dist/runtime/app/tabs/Tickets.vue +164 -0
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
  44. package/dist/runtime/app/utils/admin-event.d.ts +48 -0
  45. package/dist/runtime/app/utils/admin-event.js +22 -0
  46. package/dist/runtime/i18n/lang/da.json +34 -1
  47. package/dist/runtime/i18n/lang/en.json +33 -0
  48. package/dist/runtime/menus.js +17 -1
  49. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  50. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  51. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  52. package/dist/runtime/server/mcp/schemas.js +58 -0
  53. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  54. package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
  55. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  56. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
  57. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  58. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  59. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  60. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
  61. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  63. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
  65. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/get-event.js +12 -0
  67. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  69. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
  71. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  72. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  73. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  74. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  75. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  76. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  77. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  78. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  79. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  80. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  81. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  82. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  83. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  84. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  85. package/dist/runtime/server/routes/public/event.get.js +2 -2
  86. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  87. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  88. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  89. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  90. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  91. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  92. package/dist/runtime/server/utils/admin.d.ts +82 -1
  93. package/dist/runtime/server/utils/admin.js +51 -1
  94. package/dist/runtime/server/utils/auth.d.ts +7 -6
  95. package/dist/runtime/server/utils/auth.js +4 -4
  96. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  97. package/dist/runtime/server/utils/catalog.js +20 -4
  98. package/dist/runtime/server/utils/membership.d.ts +6 -2
  99. package/dist/runtime/server/utils/membership.js +15 -2
  100. package/dist/runtime/server/utils/tickets.js +15 -12
  101. package/dist/runtime/utils/index.d.ts +17 -7
  102. package/dist/runtime/utils/index.js +16 -19
  103. package/dist/types.d.mts +3 -1
  104. package/package.json +21 -7
@@ -1,11 +1,6 @@
1
- import { db, schema } from "@nuxthub/db";
2
- import { eq } from "drizzle-orm";
3
- import { eventInput } from "../../utils/admin-schemas.js";
4
- const patchSchema = eventInput.omit({ ticketTypes: true }).partial();
1
+ import { eventPatchSchema, patchEventRow } from "../../utils/admin.js";
5
2
  export default defineEventHandler(async (event) => {
6
3
  await requireEventsManager(event);
7
- const patch = await readValidatedBody(event, patchSchema.parse);
8
- const [row] = await db.update(schema.events).set(patch).where(eq(schema.events.id, getRouterParam(event, "id"))).returning();
9
- if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
10
- return row;
4
+ const patch = await readValidatedBody(event, eventPatchSchema.parse);
5
+ return patchEventRow(getRouterParam(event, "id"), patch);
11
6
  });
@@ -1,19 +1,9 @@
1
- import { eq } from "drizzle-orm";
2
- import { db, schema } from "@nuxthub/db";
3
- import { newId } from "../../../utils/index.js";
4
- import { insertTicketType } from "../../utils/admin.js";
1
+ import { createEventWithCatalog } from "../../utils/admin.js";
5
2
  import { eventInput } from "../../utils/admin-schemas.js";
6
3
  export default defineEventHandler(async (event) => {
7
4
  await requireEventsManager(event);
8
- const { ticketTypes, ...input } = await readValidatedBody(event, eventInput.parse);
9
- const [taken] = await db.select({ id: schema.events.id }).from(schema.events).where(eq(schema.events.slug, input.slug)).limit(1);
10
- if (taken) throw createError({ statusCode: 409, statusMessage: "An event with this slug already exists." });
11
- const [row] = await db.insert(schema.events).values({
12
- id: newId("evn"),
13
- ...input,
14
- requiresMembership: input.requiresMembership ?? null
15
- }).returning();
16
- for (const type of ticketTypes ?? []) await insertTicketType(row.id, type);
5
+ const input = await readValidatedBody(event, eventInput.parse);
6
+ const row = await createEventWithCatalog(input);
17
7
  setResponseStatus(event, 201);
18
8
  return row;
19
9
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ export default defineEventHandler(async (event) => {
2
+ await requireEventsManager(event);
3
+ return getMembershipOptions();
4
+ });
@@ -1,11 +1,6 @@
1
- import { db, schema } from "@nuxthub/db";
2
- import { eq } from "drizzle-orm";
3
- import { ticketTypeInput } from "../../utils/admin-schemas.js";
4
- const patchSchema = ticketTypeInput.omit({ addonGroups: true }).partial();
1
+ import { patchTicketTypeRow, ticketTypePatchSchema } from "../../utils/admin.js";
5
2
  export default defineEventHandler(async (event) => {
6
3
  await requireEventsManager(event);
7
- const patch = await readValidatedBody(event, patchSchema.parse);
8
- const [row] = await db.update(schema.eventTicketTypes).set(patch).where(eq(schema.eventTicketTypes.id, getRouterParam(event, "id"))).returning();
9
- if (!row) throw createError({ statusCode: 404, statusMessage: "Ticket type not found." });
10
- return row;
4
+ const patch = await readValidatedBody(event, ticketTypePatchSchema.parse);
5
+ return patchTicketTypeRow(getRouterParam(event, "id"), patch);
11
6
  });
@@ -1,8 +1,9 @@
1
- import { insertTicketType } from "../../utils/admin.js";
1
+ import { assertMembershipAvailable, insertTicketType } from "../../utils/admin.js";
2
2
  import { ticketTypeInput } from "../../utils/admin-schemas.js";
3
3
  export default defineEventHandler(async (event) => {
4
4
  await requireEventsManager(event);
5
5
  const input = await readValidatedBody(event, ticketTypeInput.parse);
6
+ await assertMembershipAvailable(input);
6
7
  const row = await insertTicketType(getRouterParam(event, "id"), input).catch((error) => {
7
8
  throw createError({ statusCode: 409, statusMessage: "Could not create the ticket type (unknown event, or the key is taken).", cause: error });
8
9
  });
@@ -2,6 +2,6 @@ export default defineEventHandler(async (event) => {
2
2
  const slug = getRouterParam(event, "slug");
3
3
  const row = await getPublishedEventRow(slug);
4
4
  if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
5
- const session = await getUserSession(event);
6
- return getEventDetail(row, session.user?.id);
5
+ const user = await getUser(event, { scopes: ["tickets:read"] });
6
+ return getEventDetail(row, user?.id);
7
7
  });
@@ -3,10 +3,11 @@ const bodySchema = z.object({
3
3
  ticketTypeId: z.string().min(1),
4
4
  addonIds: z.array(z.string().min(1)).max(50).optional(),
5
5
  successUrl: z.string().optional(),
6
- cancelUrl: z.string().optional()
6
+ cancelUrl: z.string().optional(),
7
+ locale: z.string().regex(/^[a-z]{2,3}(?:[-_][a-z0-9]{2,8})*$/i).max(16).optional()
7
8
  });
8
9
  export default defineEventHandler(async (event) => {
9
- const { user } = await requireUserSession(event);
10
+ const user = await requireUser(event, { scopes: ["tickets:write"] });
10
11
  const body = await readValidatedBody(event, bodySchema.parse);
11
12
  return purchaseTicket(event, user.id, getRouterParam(event, "slug"), body);
12
13
  });
@@ -1,7 +1,7 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq } from "drizzle-orm";
3
3
  export default defineEventHandler(async (event) => {
4
- const { user } = await requireUserSession(event);
4
+ const user = await requireUser(event, { scopes: ["tickets:write"] });
5
5
  const [ticket] = await db.select().from(schema.eventTickets).where(and(eq(schema.eventTickets.id, getRouterParam(event, "id")), eq(schema.eventTickets.userId, user.id))).limit(1);
6
6
  if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
7
7
  if (ticket.status === "paid") throw createError({ statusCode: 409, statusMessage: "This ticket is already paid." });
@@ -1,4 +1,4 @@
1
1
  export default defineEventHandler(async (event) => {
2
- const { user } = await requireUserSession(event);
2
+ const user = await requireUser(event, { scopes: ["tickets:read"] });
3
3
  return getUserTicket(user.id, getRouterParam(event, "id"));
4
4
  });
@@ -1,4 +1,4 @@
1
1
  export default defineEventHandler(async (event) => {
2
- const { user } = await requireUserSession(event);
2
+ const user = await requireUser(event, { scopes: ["tickets:read"] });
3
3
  return listUserTickets(user.id);
4
4
  });
@@ -1,21 +1,33 @@
1
1
  import { z } from 'zod';
2
- export declare const translated: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
2
+ export declare const translated: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
3
+ [k: string]: string;
4
+ }, Record<string, string>>>]>;
3
5
  export declare const membership: z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>;
4
6
  export declare const addonInput: z.ZodObject<{
5
- label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
6
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
7
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
8
+ [k: string]: string;
9
+ }, Record<string, string>>>]>;
10
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
11
+ [k: string]: string;
12
+ }, Record<string, string>>>]>>>;
7
13
  price: z.ZodOptional<z.ZodNumber>;
8
14
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
9
15
  sortOrder: z.ZodOptional<z.ZodNumber>;
10
16
  }, z.core.$strip>;
11
17
  export declare const addonGroupInput: z.ZodObject<{
12
- title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
18
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
19
+ [k: string]: string;
20
+ }, Record<string, string>>>]>;
13
21
  required: z.ZodOptional<z.ZodBoolean>;
14
22
  multiple: z.ZodOptional<z.ZodBoolean>;
15
23
  sortOrder: z.ZodOptional<z.ZodNumber>;
16
24
  addons: z.ZodOptional<z.ZodArray<z.ZodObject<{
17
- label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
18
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
25
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
26
+ [k: string]: string;
27
+ }, Record<string, string>>>]>;
28
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
29
+ [k: string]: string;
30
+ }, Record<string, string>>>]>>>;
19
31
  price: z.ZodOptional<z.ZodNumber>;
20
32
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
21
33
  sortOrder: z.ZodOptional<z.ZodNumber>;
@@ -23,26 +35,57 @@ export declare const addonGroupInput: z.ZodObject<{
23
35
  }, z.core.$strip>;
24
36
  export declare const ticketTypeInput: z.ZodObject<{
25
37
  key: z.ZodString;
26
- name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
27
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
38
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
39
+ [k: string]: string;
40
+ }, Record<string, string>>>]>;
41
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
42
+ [k: string]: string;
43
+ }, Record<string, string>>>]>>>;
28
44
  price: z.ZodOptional<z.ZodNumber>;
29
45
  currency: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
30
46
  hidePrice: z.ZodOptional<z.ZodBoolean>;
31
47
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
32
48
  saleStartsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
33
49
  saleEndsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
50
+ saleSchedule: z.ZodOptional<z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
51
+ type: z.ZodLiteral<"always">;
52
+ }, z.core.$strip>, z.ZodObject<{
53
+ type: z.ZodLiteral<"range">;
54
+ from: z.ZodOptional<z.ZodString>;
55
+ to: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strip>, z.ZodObject<{
57
+ type: z.ZodLiteral<"yearly">;
58
+ from: z.ZodObject<{
59
+ month: z.ZodNumber;
60
+ day: z.ZodNumber;
61
+ time: z.ZodString;
62
+ }, z.core.$strip>;
63
+ to: z.ZodObject<{
64
+ month: z.ZodNumber;
65
+ day: z.ZodNumber;
66
+ time: z.ZodString;
67
+ }, z.core.$strip>;
68
+ timezone: z.ZodString;
69
+ }, z.core.$strip>], "type">>>;
34
70
  maxPerUser: z.ZodOptional<z.ZodNumber>;
35
71
  requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
72
+ excludesMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
36
73
  isDefault: z.ZodOptional<z.ZodBoolean>;
37
74
  sortOrder: z.ZodOptional<z.ZodNumber>;
38
75
  addonGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
39
- title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
76
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
77
+ [k: string]: string;
78
+ }, Record<string, string>>>]>;
40
79
  required: z.ZodOptional<z.ZodBoolean>;
41
80
  multiple: z.ZodOptional<z.ZodBoolean>;
42
81
  sortOrder: z.ZodOptional<z.ZodNumber>;
43
82
  addons: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
- label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
45
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
83
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
84
+ [k: string]: string;
85
+ }, Record<string, string>>>]>;
86
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
87
+ [k: string]: string;
88
+ }, Record<string, string>>>]>>>;
46
89
  price: z.ZodOptional<z.ZodNumber>;
47
90
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
48
91
  sortOrder: z.ZodOptional<z.ZodNumber>;
@@ -51,11 +94,19 @@ export declare const ticketTypeInput: z.ZodObject<{
51
94
  }, z.core.$strip>;
52
95
  export declare const eventInput: z.ZodObject<{
53
96
  slug: z.ZodString;
54
- name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
55
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
56
- body: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
97
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
98
+ [k: string]: string;
99
+ }, Record<string, string>>>]>;
100
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
101
+ [k: string]: string;
102
+ }, Record<string, string>>>]>>>;
103
+ body: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
104
+ [k: string]: string;
105
+ }, Record<string, string>>>]>>>;
57
106
  image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
- location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
107
+ location: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
108
+ [k: string]: string;
109
+ }, Record<string, string>>>]>>>;
59
110
  locationUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
60
111
  startsAt: z.ZodCoercedDate<unknown>;
61
112
  endsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
@@ -67,26 +118,57 @@ export declare const eventInput: z.ZodObject<{
67
118
  requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
68
119
  ticketTypes: z.ZodOptional<z.ZodArray<z.ZodObject<{
69
120
  key: z.ZodString;
70
- name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
71
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
121
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
122
+ [k: string]: string;
123
+ }, Record<string, string>>>]>;
124
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
125
+ [k: string]: string;
126
+ }, Record<string, string>>>]>>>;
72
127
  price: z.ZodOptional<z.ZodNumber>;
73
128
  currency: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
74
129
  hidePrice: z.ZodOptional<z.ZodBoolean>;
75
130
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
76
131
  saleStartsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
77
132
  saleEndsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
133
+ saleSchedule: z.ZodOptional<z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
134
+ type: z.ZodLiteral<"always">;
135
+ }, z.core.$strip>, z.ZodObject<{
136
+ type: z.ZodLiteral<"range">;
137
+ from: z.ZodOptional<z.ZodString>;
138
+ to: z.ZodOptional<z.ZodString>;
139
+ }, z.core.$strip>, z.ZodObject<{
140
+ type: z.ZodLiteral<"yearly">;
141
+ from: z.ZodObject<{
142
+ month: z.ZodNumber;
143
+ day: z.ZodNumber;
144
+ time: z.ZodString;
145
+ }, z.core.$strip>;
146
+ to: z.ZodObject<{
147
+ month: z.ZodNumber;
148
+ day: z.ZodNumber;
149
+ time: z.ZodString;
150
+ }, z.core.$strip>;
151
+ timezone: z.ZodString;
152
+ }, z.core.$strip>], "type">>>;
78
153
  maxPerUser: z.ZodOptional<z.ZodNumber>;
79
154
  requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
155
+ excludesMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
80
156
  isDefault: z.ZodOptional<z.ZodBoolean>;
81
157
  sortOrder: z.ZodOptional<z.ZodNumber>;
82
158
  addonGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
83
- title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
159
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
160
+ [k: string]: string;
161
+ }, Record<string, string>>>]>;
84
162
  required: z.ZodOptional<z.ZodBoolean>;
85
163
  multiple: z.ZodOptional<z.ZodBoolean>;
86
164
  sortOrder: z.ZodOptional<z.ZodNumber>;
87
165
  addons: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
- label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
89
- description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
166
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
167
+ [k: string]: string;
168
+ }, Record<string, string>>>]>;
169
+ description: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
170
+ [k: string]: string;
171
+ }, Record<string, string>>>]>>>;
90
172
  price: z.ZodOptional<z.ZodNumber>;
91
173
  capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
92
174
  sortOrder: z.ZodOptional<z.ZodNumber>;
@@ -1,5 +1,9 @@
1
1
  import { z } from "zod";
2
- export const translated = z.union([z.string().min(1), z.record(z.string(), z.string())]);
2
+ import { scheduleSchema } from "@frigear-nu/module-kit/schedule";
3
+ export const translated = z.union([
4
+ z.string().min(1),
5
+ z.record(z.string(), z.string()).transform((value) => Object.fromEntries(Object.entries(value).filter(([, text]) => text.trim()))).refine((value) => Object.keys(value).length > 0, "Add a text in at least one language.")
6
+ ]);
3
7
  export const membership = z.union([z.literal("any"), z.array(z.string().min(1))]).nullable();
4
8
  const date = z.coerce.date();
5
9
  export const addonInput = z.object({
@@ -26,8 +30,10 @@ export const ticketTypeInput = z.object({
26
30
  capacity: z.number().int().min(0).nullish(),
27
31
  saleStartsAt: date.nullish(),
28
32
  saleEndsAt: date.nullish(),
33
+ saleSchedule: scheduleSchema.nullish(),
29
34
  maxPerUser: z.number().int().min(1).optional(),
30
35
  requiresMembership: membership.optional(),
36
+ excludesMembership: membership.optional(),
31
37
  isDefault: z.boolean().optional(),
32
38
  sortOrder: z.number().int().optional(),
33
39
  addonGroups: z.array(addonGroupInput).optional()
@@ -38,7 +44,7 @@ export const eventInput = z.object({
38
44
  description: translated.nullish(),
39
45
  body: translated.nullish(),
40
46
  image: z.string().nullish(),
41
- location: z.string().nullish(),
47
+ location: translated.nullish(),
42
48
  locationUrl: z.string().nullish(),
43
49
  startsAt: date,
44
50
  endsAt: date.nullish(),
@@ -1,7 +1,88 @@
1
1
  import type { z } from 'zod';
2
- import type { addonGroupInput, ticketTypeInput } from './admin-schemas.js';
2
+ import { eventInput, ticketTypeInput } from './admin-schemas.js';
3
+ import type { addonGroupInput } from './admin-schemas.js';
3
4
  type TicketTypeInput = z.output<typeof ticketTypeInput>;
4
5
  type AddonGroupInput = z.output<typeof addonGroupInput>;
5
6
  export declare function insertAddonGroup(ticketTypeId: string, input: AddonGroupInput): Promise<any>;
6
7
  export declare function insertTicketType(eventId: string, input: TicketTypeInput): Promise<any>;
8
+ /**
9
+ * Membership rules only make sense with a membership provider (`@frigear-nu/subscriptions`): without one a
10
+ * requirement could never be met (and would silently lock the ticket), so staff can't set one.
11
+ */
12
+ export declare function assertMembershipAvailable(...inputs: Array<{
13
+ requiresMembership?: unknown;
14
+ excludesMembership?: unknown;
15
+ } | undefined>): Promise<void>;
16
+ type EventInput = z.output<typeof eventInput>;
17
+ /** Creates an event, optionally with its whole catalogue (ticket types -> addon groups -> addons). */
18
+ export declare function createEventWithCatalog(raw: EventInput): Promise<any>;
19
+ export declare const eventPatchSchema: z.ZodObject<{
20
+ name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
21
+ [k: string]: string;
22
+ }, Record<string, string>>>]>>;
23
+ status: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
24
+ draft: "draft";
25
+ published: "published";
26
+ archived: "archived";
27
+ }>>>;
28
+ slug: z.ZodOptional<z.ZodString>;
29
+ description: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
30
+ [k: string]: string;
31
+ }, Record<string, string>>>]>>>>;
32
+ body: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
33
+ [k: string]: string;
34
+ }, Record<string, string>>>]>>>>;
35
+ image: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
36
+ location: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
37
+ [k: string]: string;
38
+ }, Record<string, string>>>]>>>>;
39
+ locationUrl: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
40
+ startsAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
41
+ endsAt: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
42
+ requiresMembership: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>>;
43
+ }, z.core.$strip>;
44
+ export declare function patchEventRow(id: string, patch: z.output<typeof eventPatchSchema>): Promise<any>;
45
+ export declare const ticketTypePatchSchema: z.ZodObject<{
46
+ currency: z.ZodOptional<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
47
+ name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
48
+ [k: string]: string;
49
+ }, Record<string, string>>>]>>;
50
+ key: z.ZodOptional<z.ZodString>;
51
+ price: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
52
+ hidePrice: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
53
+ description: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
54
+ [k: string]: string;
55
+ }, Record<string, string>>>]>>>>;
56
+ requiresMembership: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>>;
57
+ capacity: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>;
58
+ saleStartsAt: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
59
+ saleEndsAt: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
60
+ saleSchedule: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
61
+ type: z.ZodLiteral<"always">;
62
+ }, z.core.$strip>, z.ZodObject<{
63
+ type: z.ZodLiteral<"range">;
64
+ from: z.ZodOptional<z.ZodString>;
65
+ to: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strip>, z.ZodObject<{
67
+ type: z.ZodLiteral<"yearly">;
68
+ from: z.ZodObject<{
69
+ month: z.ZodNumber;
70
+ day: z.ZodNumber;
71
+ time: z.ZodString;
72
+ }, z.core.$strip>;
73
+ to: z.ZodObject<{
74
+ month: z.ZodNumber;
75
+ day: z.ZodNumber;
76
+ time: z.ZodString;
77
+ }, z.core.$strip>;
78
+ timezone: z.ZodString;
79
+ }, z.core.$strip>], "type">>>>;
80
+ maxPerUser: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
81
+ excludesMembership: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>>;
82
+ isDefault: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
83
+ sortOrder: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
84
+ }, z.core.$strip>;
85
+ export declare function patchTicketTypeRow(id: string, patch: z.output<typeof ticketTypePatchSchema>): Promise<any>;
86
+ /** One event with all of its ticket types (capacity and sale window included), whatever its status. */
87
+ export declare function getEventWithTicketTypes(id: string): Promise<any>;
7
88
  export {};
@@ -1,5 +1,8 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { newId } from "../../utils/index.js";
3
+ import { asc, eq } from "drizzle-orm";
4
+ import { loadCatalog } from "./catalog.js";
5
+ import { eventInput, ticketTypeInput } from "./admin-schemas.js";
3
6
  export async function insertAddonGroup(ticketTypeId, input) {
4
7
  const { addons, ...group } = input;
5
8
  const [row] = await db.insert(schema.eventAddonGroups).values({ id: newId("agr"), ticketTypeId, ...group }).returning();
@@ -17,8 +20,55 @@ export async function insertTicketType(eventId, input) {
17
20
  capacity: type.capacity ?? null,
18
21
  saleStartsAt: type.saleStartsAt ?? null,
19
22
  saleEndsAt: type.saleEndsAt ?? null,
20
- requiresMembership: type.requiresMembership ?? null
23
+ saleSchedule: type.saleSchedule ?? null,
24
+ requiresMembership: type.requiresMembership ?? null,
25
+ excludesMembership: type.excludesMembership ?? null
21
26
  }).returning();
22
27
  for (const group of addonGroups ?? []) await insertAddonGroup(row.id, group);
23
28
  return row;
24
29
  }
30
+ export async function assertMembershipAvailable(...inputs) {
31
+ if (!inputs.some((input) => input?.requiresMembership || input?.excludesMembership)) return;
32
+ if ((await getMembershipOptions()).available) return;
33
+ throw createError({ statusCode: 400, statusMessage: "Membership rules need a membership provider such as @frigear-nu/subscriptions." });
34
+ }
35
+ export async function createEventWithCatalog(raw) {
36
+ const { ticketTypes, ...input } = raw;
37
+ await assertMembershipAvailable(input, ...ticketTypes ?? []);
38
+ const [taken] = await db.select({ id: schema.events.id }).from(schema.events).where(eq(schema.events.slug, input.slug)).limit(1);
39
+ if (taken) throw createError({ statusCode: 409, statusMessage: "An event with this slug already exists." });
40
+ const [row] = await db.insert(schema.events).values({
41
+ id: newId("evn"),
42
+ ...input,
43
+ requiresMembership: input.requiresMembership ?? null
44
+ }).returning();
45
+ for (const type of ticketTypes ?? []) await insertTicketType(row.id, type);
46
+ return row;
47
+ }
48
+ export const eventPatchSchema = eventInput.omit({ ticketTypes: true }).partial();
49
+ export async function patchEventRow(id, patch) {
50
+ await assertMembershipAvailable(patch);
51
+ const [row] = await db.update(schema.events).set(patch).where(eq(schema.events.id, id)).returning();
52
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
53
+ return row;
54
+ }
55
+ export const ticketTypePatchSchema = ticketTypeInput.omit({ addonGroups: true }).partial();
56
+ export async function patchTicketTypeRow(id, patch) {
57
+ await assertMembershipAvailable(patch);
58
+ const [row] = await db.update(schema.eventTicketTypes).set(patch).where(eq(schema.eventTicketTypes.id, id)).returning();
59
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Ticket type not found." });
60
+ return row;
61
+ }
62
+ export async function getEventWithTicketTypes(id) {
63
+ const [row] = await db.select().from(schema.events).where(eq(schema.events.id, id)).limit(1);
64
+ if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
65
+ const ticketTypes = await db.select().from(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.eventId, id)).orderBy(asc(schema.eventTicketTypes.sortOrder));
66
+ const { groups, addons } = await loadCatalog(id);
67
+ return {
68
+ ...row,
69
+ ticketTypes: ticketTypes.map((type) => ({
70
+ ...type,
71
+ addonGroups: groups.filter((group) => group.ticketTypeId === type.id).map((group) => ({ ...group, addons: addons.filter((addon) => addon.groupId === group.id) }))
72
+ }))
73
+ };
74
+ }
@@ -1,10 +1,11 @@
1
1
  import type { H3Event } from 'h3';
2
- /**
3
- * Gate for staff routes (event/ticket management, check-in). This repo has no shared permission
4
- * system, so the host app decides through the `events:authorize` Nitro hook. Denied by default:
5
- *
6
- * nitroApp.hooks.hook('events:authorize', (ctx) => { ctx.allowed = isStaff(ctx.userId) })
7
- */
2
+ /** Gate for staff routes (event/ticket management, check-in). */
8
3
  export declare function requireEventsManager(event: H3Event): Promise<{
9
4
  userId: number;
10
5
  }>;
6
+ /** Non-throwing form of the `events:authorize` policy for the caller of an already-authenticated request (MCP). */
7
+ export declare const canManageEvents: (event: H3Event, principal: {
8
+ user: {
9
+ id: number;
10
+ };
11
+ }) => any;
@@ -1,7 +1,7 @@
1
+ const gate = definePolicyGate("events:authorize");
1
2
  export async function requireEventsManager(event) {
2
- const { user } = await requireUserSession(event);
3
- const ctx = { event, userId: user.id, allowed: false };
4
- await useNitroApp().hooks.callHook("events:authorize", ctx);
5
- if (!ctx.allowed) throw createError({ statusCode: 403, statusMessage: "You are not allowed to manage events." });
3
+ const user = await requireUser(event, { scopes: ["events:manage"] });
4
+ await gate.assert(event, user.id, "You are not allowed to manage events.");
6
5
  return { userId: user.id };
7
6
  }
7
+ export const canManageEvents = (event, principal) => gate.isAllowed(event, principal.user.id);
@@ -1,5 +1,6 @@
1
1
  import { schema } from '@nuxthub/db';
2
2
  import type { EventDetailDto, EventSummaryDto, TicketDto } from '../../../types.js';
3
+ import type { Schedule } from '@frigear-nu/module-kit/schedule';
3
4
  type EventRow = typeof schema.events.$inferSelect;
4
5
  type TicketRow = typeof schema.eventTickets.$inferSelect;
5
6
  /** Tickets that currently occupy a seat: paid ones, and unpaid ones still inside their reservation window. */
@@ -15,6 +16,19 @@ export declare function loadCatalog(eventId: string): Promise<{
15
16
  groups: any;
16
17
  addons: any;
17
18
  }>;
19
+ type SaleWindowType = {
20
+ saleStartsAt: Date | null;
21
+ saleEndsAt: Date | null;
22
+ saleSchedule: Schedule | null;
23
+ };
24
+ /**
25
+ * Whether a ticket type can be bought at `now`: its fixed window (`saleStartsAt`/`saleEndsAt`) and its optional
26
+ * recurring `saleSchedule` must both be open. `opensAt` is when sales (re)open, if they will.
27
+ */
28
+ export declare function saleWindow(type: SaleWindowType, now?: Date): {
29
+ state: 'open' | 'not_started' | 'ended';
30
+ opensAt: Date | null;
31
+ };
18
32
  /**
19
33
  * The public event detail. For a signed-in user (`userId`) each ticket type also says whether they
20
34
  * may buy it (`blocked`), so the UI can disable it with the right badge.
@@ -1,7 +1,8 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, asc, eq, gt, inArray, like, or, sql } from "drizzle-orm";
3
+ import { isScheduleOpen, nextScheduleOpening } from "@frigear-nu/module-kit/schedule";
3
4
  import { normalizeMembership } from "../../utils/index.js";
4
- import { meetsMembership } from "./membership.js";
5
+ import { meetsMembership, passesExclusion } from "./membership.js";
5
6
  export function holdsSeat() {
6
7
  const cutoff = new Date(Date.now() - useRuntimeConfig().events.reservationMinutes * 6e4);
7
8
  return or(
@@ -46,6 +47,16 @@ export async function loadCatalog(eventId) {
46
47
  const addons = groupIds.length ? await db.select().from(schema.eventAddons).where(inArray(schema.eventAddons.groupId, groupIds)).orderBy(asc(schema.eventAddons.sortOrder)) : [];
47
48
  return { types, groups, addons };
48
49
  }
50
+ export function saleWindow(type, now = /* @__PURE__ */ new Date()) {
51
+ const time = now.getTime();
52
+ if (type.saleStartsAt && type.saleStartsAt.getTime() > time) return { state: "not_started", opensAt: type.saleStartsAt };
53
+ if (type.saleEndsAt && type.saleEndsAt.getTime() < time) return { state: "ended", opensAt: null };
54
+ if (!isScheduleOpen(type.saleSchedule ?? void 0, now)) {
55
+ const opensAt = nextScheduleOpening(type.saleSchedule ?? void 0, now);
56
+ return opensAt && (!type.saleEndsAt || opensAt < type.saleEndsAt) ? { state: "not_started", opensAt } : { state: "ended", opensAt: null };
57
+ }
58
+ return { state: "open", opensAt: null };
59
+ }
49
60
  export async function getEventDetail(row, userId) {
50
61
  const { types, groups, addons } = await loadCatalog(row.id);
51
62
  const eventRequirement = normalizeMembership(row.requiresMembership);
@@ -53,11 +64,14 @@ export async function getEventDetail(row, userId) {
53
64
  const ticketTypes = await Promise.all(types.map(async (type) => {
54
65
  const seats = type.capacity === null ? null : await countSeats(type.id);
55
66
  const remaining = type.capacity === null ? null : Math.max(type.capacity - (seats ?? 0), 0);
56
- const saleState = type.saleStartsAt && type.saleStartsAt.getTime() > now ? "not_started" : type.saleEndsAt && type.saleEndsAt.getTime() < now ? "ended" : remaining === 0 ? "sold_out" : "open";
67
+ const window = saleWindow(type, new Date(now));
68
+ const saleState = window.state === "open" && remaining === 0 ? "sold_out" : window.state;
57
69
  const typeRequirement = normalizeMembership(type.requiresMembership);
70
+ const typeExclusion = normalizeMembership(type.excludesMembership);
58
71
  let blocked = null;
59
72
  if (userId !== void 0) {
60
73
  if (!await meetsMembership(userId, eventRequirement) || !await meetsMembership(userId, typeRequirement)) blocked = "membership_required";
74
+ else if (!await passesExclusion(userId, typeExclusion)) blocked = "membership_excluded";
61
75
  else {
62
76
  const [owned] = await db.select({ count: sql`count(*)` }).from(schema.eventTickets).where(and(
63
77
  eq(schema.eventTickets.userId, userId),
@@ -90,8 +104,9 @@ export async function getEventDetail(row, userId) {
90
104
  hidePrice: type.hidePrice,
91
105
  isDefault: type.isDefault,
92
106
  requiresMembership: typeRequirement,
107
+ excludesMembership: typeExclusion,
93
108
  remaining,
94
- saleStartsAt: type.saleStartsAt?.toISOString() ?? null,
109
+ saleStartsAt: (window.state === "not_started" ? window.opensAt : type.saleStartsAt)?.toISOString() ?? null,
95
110
  saleEndsAt: type.saleEndsAt?.toISOString() ?? null,
96
111
  saleState,
97
112
  blocked,
@@ -123,7 +138,8 @@ export async function toTicketDtos(rows) {
123
138
  paymentId: row.paymentId,
124
139
  code: row.status === "paid" ? row.code : null,
125
140
  checkedInAt: row.checkedInAt?.toISOString() ?? null,
126
- createdAt: row.createdAt.toISOString()
141
+ createdAt: row.createdAt.toISOString(),
142
+ locale: row.locale ?? null
127
143
  };
128
144
  });
129
145
  }