@frigear-nu/events 0.0.7 → 0.0.9

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 (114) 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 +59 -41
  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/app/widgets/UpcomingEvents.d.vue.ts +3 -0
  47. package/dist/runtime/app/widgets/UpcomingEvents.vue +39 -0
  48. package/dist/runtime/app/widgets/UpcomingEvents.vue.d.ts +3 -0
  49. package/dist/runtime/app/widgets/UpcomingEventsStat.d.vue.ts +3 -0
  50. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +17 -0
  51. package/dist/runtime/app/widgets/UpcomingEventsStat.vue.d.ts +3 -0
  52. package/dist/runtime/i18n/lang/da.json +33 -0
  53. package/dist/runtime/i18n/lang/en.json +33 -0
  54. package/dist/runtime/menus.js +15 -1
  55. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  56. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  57. package/dist/runtime/server/mcp/money.d.ts +24 -0
  58. package/dist/runtime/server/mcp/money.js +42 -0
  59. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  60. package/dist/runtime/server/mcp/schemas.js +59 -0
  61. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/create-event.js +12 -0
  63. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +11 -0
  65. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  67. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +26 -0
  69. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  71. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  72. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +17 -0
  73. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  74. package/dist/runtime/server/mcp/tools/get-event.js +13 -0
  75. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  76. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  77. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  78. package/dist/runtime/server/mcp/tools/my-tickets.js +7 -0
  79. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  80. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  81. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  82. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  83. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  84. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  85. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  86. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  87. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  88. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  89. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  90. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  91. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  92. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  93. package/dist/runtime/server/routes/admin/upcoming.get.d.ts +2 -0
  94. package/dist/runtime/server/routes/admin/upcoming.get.js +17 -0
  95. package/dist/runtime/server/routes/public/event.get.js +2 -2
  96. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  97. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  98. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  99. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  100. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  101. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  102. package/dist/runtime/server/utils/admin.d.ts +82 -1
  103. package/dist/runtime/server/utils/admin.js +51 -1
  104. package/dist/runtime/server/utils/auth.d.ts +7 -6
  105. package/dist/runtime/server/utils/auth.js +4 -4
  106. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  107. package/dist/runtime/server/utils/catalog.js +20 -4
  108. package/dist/runtime/server/utils/membership.d.ts +6 -2
  109. package/dist/runtime/server/utils/membership.js +15 -2
  110. package/dist/runtime/server/utils/tickets.js +15 -12
  111. package/dist/runtime/utils/index.d.ts +17 -7
  112. package/dist/runtime/utils/index.js +16 -19
  113. package/dist/types.d.mts +3 -1
  114. package/package.json +20 -7
@@ -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
  }
@@ -1,3 +1,7 @@
1
- import type { MembershipRequirement } from '../../types.js';
2
- /** True when `requirement` is null, or the user holds a matching active subscription. */
1
+ import type { MembershipOptionsContext, MembershipRequirement } from '../../types.js';
2
+ /** True when `requirement` is null, or a membership provider confirms the user holds a matching membership. */
3
3
  export declare function meetsMembership(userId: number, requirement: MembershipRequirement | undefined): Promise<boolean>;
4
+ /** True when `exclusion` is null, or the user holds none of the excluded memberships (`'any'` = no membership at all). */
5
+ export declare function passesExclusion(userId: number, exclusion: MembershipRequirement | undefined): Promise<boolean>;
6
+ /** What the installed membership provider(s) offer; `available` is false when there is none. */
7
+ export declare function getMembershipOptions(): Promise<MembershipOptionsContext>;
@@ -1,5 +1,18 @@
1
- import { hasActiveSubscription } from "#events/membership";
1
+ async function holds(userId, requirement) {
2
+ const ctx = { userId, requirement, satisfied: false };
3
+ await useNitroApp().hooks.callHook("events:membership:check", ctx);
4
+ return ctx.satisfied;
5
+ }
2
6
  export async function meetsMembership(userId, requirement) {
3
7
  if (!requirement) return true;
4
- return hasActiveSubscription(userId, requirement === "any" ? {} : { plans: requirement });
8
+ return holds(userId, requirement);
9
+ }
10
+ export async function passesExclusion(userId, exclusion) {
11
+ if (!exclusion) return true;
12
+ return !await holds(userId, exclusion);
13
+ }
14
+ export async function getMembershipOptions() {
15
+ const ctx = { available: false, options: [] };
16
+ await useNitroApp().hooks.callHook("events:membership:options", ctx);
17
+ return ctx;
5
18
  }
@@ -1,8 +1,9 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq, sql } from "drizzle-orm";
3
- import { newId, newTicketCode, normalizeMembership, resolveSameOriginUrl, translate } from "../../utils/index.js";
3
+ import { resolveSameOriginUrl } from "@frigear-nu/stripe/utils";
4
+ import { newId, newTicketCode, normalizeMembership, translate } from "../../utils/index.js";
4
5
  import { countAddonSeats, countSeats, getPublishedEventRow, holdsSeat, loadCatalog, toTicketDtos } from "./catalog.js";
5
- import { meetsMembership } from "./membership.js";
6
+ import { meetsMembership, passesExclusion } from "./membership.js";
6
7
  const fail = (statusCode, statusMessage) => createError({ statusCode, statusMessage });
7
8
  function returnUrls(event, slug, ticketId, body) {
8
9
  const origin = getRequestURL(event).origin;
@@ -30,13 +31,14 @@ export async function purchaseTicket(event, userId, slug, body) {
30
31
  const { types, groups, addons } = await loadCatalog(eventRow.id);
31
32
  const type = types.find((item) => item.id === body.ticketTypeId);
32
33
  if (!type) throw fail(404, "Ticket type not found.");
33
- const now = Date.now();
34
- if (type.saleStartsAt && type.saleStartsAt.getTime() > now) throw fail(409, "Ticket sales have not started yet.");
35
- if (type.saleEndsAt && type.saleEndsAt.getTime() < now) throw fail(409, "Ticket sales have ended.");
34
+ const window = saleWindow(type);
35
+ if (window.state === "not_started") throw fail(409, "Ticket sales have not started yet.");
36
+ if (window.state === "ended") throw fail(409, "Ticket sales have ended.");
36
37
  const requirements = [normalizeMembership(eventRow.requiresMembership), normalizeMembership(type.requiresMembership)];
37
38
  for (const requirement of requirements) {
38
39
  if (!await meetsMembership(userId, requirement)) throw fail(403, "An active membership is required for this ticket.");
39
40
  }
41
+ if (!await passesExclusion(userId, normalizeMembership(type.excludesMembership))) throw fail(403, "This ticket is not available for your membership.");
40
42
  const typeGroups = groups.filter((group) => group.ticketTypeId === type.id);
41
43
  const typeAddons = addons.filter((addon) => typeGroups.some((group) => group.id === addon.groupId));
42
44
  const addonIds = [...new Set(body.addonIds ?? [])];
@@ -47,11 +49,11 @@ export async function purchaseTicket(event, userId, slug, body) {
47
49
  });
48
50
  for (const group of typeGroups) {
49
51
  const inGroup = chosen.filter((addon) => addon.groupId === group.id);
50
- if (group.required && inGroup.length === 0) throw fail(400, `Choose an option for "${translate(group.title)}".`);
51
- if (!group.multiple && inGroup.length > 1) throw fail(400, `Only one option can be chosen for "${translate(group.title)}".`);
52
+ if (group.required && inGroup.length === 0) throw fail(400, `Choose an option for "${translate(group.title, body.locale)}".`);
53
+ if (!group.multiple && inGroup.length > 1) throw fail(400, `Only one option can be chosen for "${translate(group.title, body.locale)}".`);
52
54
  }
53
55
  for (const addon of chosen) {
54
- if (addon.capacity !== null && await countAddonSeats(addon.id) >= addon.capacity) throw fail(409, `"${translate(addon.label)}" is sold out.`);
56
+ if (addon.capacity !== null && await countAddonSeats(addon.id) >= addon.capacity) throw fail(409, `"${translate(addon.label, body.locale)}" is sold out.`);
55
57
  }
56
58
  if (type.capacity !== null && await countSeats(type.id) >= type.capacity) throw fail(409, "This ticket type is sold out.");
57
59
  await useNitroApp().hooks.callHook("events:purchase:before", { event, userId, eventId: eventRow.id, ticketTypeId: type.id, addonIds });
@@ -64,6 +66,7 @@ export async function purchaseTicket(event, userId, slug, body) {
64
66
  eventId: eventRow.id,
65
67
  ticketTypeId: type.id,
66
68
  userId,
69
+ locale: body.locale ?? null,
67
70
  status: amount === 0 ? "paid" : "pending",
68
71
  addons: chosen.map((addon) => ({ id: addon.id, label: addon.label, price: addon.price })),
69
72
  amount,
@@ -81,7 +84,7 @@ export async function purchaseTicket(event, userId, slug, body) {
81
84
  const winners = await db.select({ id: schema.eventTickets.id }).from(schema.eventTickets).where(and(sql`${schema.eventTickets.addons} like ${`%"id":"${addon.id}"%`}`, holdsSeat())).orderBy(sql`rowid`).limit(addon.capacity);
82
85
  if (!winners.some((winner) => winner.id === id)) {
83
86
  await release(id);
84
- throw fail(409, `"${translate(addon.label)}" is sold out.`);
87
+ throw fail(409, `"${translate(addon.label, body.locale)}" is sold out.`);
85
88
  }
86
89
  }
87
90
  const [dto] = await toTicketDtos([inserted]);
@@ -93,10 +96,10 @@ export async function purchaseTicket(event, userId, slug, body) {
93
96
  const urls = returnUrls(event, eventRow.slug, id, body);
94
97
  const { payment, url } = await createPayment({
95
98
  userId,
96
- description: `${translate(eventRow.name)} - ${translate(type.name)}`,
99
+ description: `${translate(eventRow.name, body.locale)} - ${translate(type.name, body.locale)}`,
97
100
  items: [
98
- ...type.price > 0 ? [{ name: `${translate(eventRow.name)} - ${translate(type.name)}`, amount: type.price, currency: type.currency }] : [],
99
- ...chosen.filter((addon) => addon.price > 0).map((addon) => ({ name: translate(addon.label), amount: addon.price, currency: type.currency }))
101
+ ...type.price > 0 ? [{ name: `${translate(eventRow.name, body.locale)} - ${translate(type.name, body.locale)}`, amount: type.price, currency: type.currency }] : [],
102
+ ...chosen.filter((addon) => addon.price > 0).map((addon) => ({ name: translate(addon.label, body.locale), amount: addon.price, currency: type.currency }))
100
103
  ],
101
104
  reference: { type: "event_ticket", id },
102
105
  expiresInMinutes: useRuntimeConfig().events.reservationMinutes,
@@ -1,14 +1,24 @@
1
- import type { MembershipRequirement, Translated } from '../types.js';
1
+ import { translate } from '@frigear-nu/ui/translated';
2
+ import type { MembershipRequirement } from '../types.js';
3
+ export { translate };
2
4
  export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, MembershipRequirement, ModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated, } from '../types.js';
3
- /** Picks the text for a locale, falling back to `en`, then to the first available entry. */
4
- export declare function translate(value: Translated | null | undefined, locale?: string): string;
5
- /** Formats minor units, e.g. `formatPrice(15000, 'DKK', 'da')` -> `150,00 kr.`. */
6
- export declare function formatPrice(amount: number, currency: string, locale?: string): string;
5
+ /** The currency tickets and add-ons are sold in unless they say otherwise (the column default). */
6
+ export declare const DEFAULT_CURRENCY = "DKK";
7
7
  /** Normalises a stored requirement: empty plan lists mean "any active subscription". */
8
8
  export declare function normalizeMembership(value: MembershipRequirement | undefined): MembershipRequirement;
9
- /** Resolves a relative path against `origin`, and only accepts absolute URLs on the same origin. */
10
- export declare function resolveSameOriginUrl(input: string, origin: string): string | null;
11
9
  /** Random, prefixed id (`tkt_...`), Web Crypto only so it works on Workers. */
12
10
  export declare function newId(prefix: string): string;
13
11
  /** A longer random token for ticket QR codes. */
14
12
  export declare function newTicketCode(): string;
13
+ /**
14
+ * Variables a ticket type's description can use in MDC (`{{ticket}}`, `{{price}}`, ...).
15
+ * `ticket` is the ticket's name; the rest describe the same ticket.
16
+ */
17
+ export declare function ticketMdcData(type: {
18
+ name: unknown;
19
+ price: number;
20
+ currency: string;
21
+ hidePrice?: boolean;
22
+ remaining?: number | null;
23
+ key?: string;
24
+ }, name: string, locale?: string): Record<string, unknown>;
@@ -1,30 +1,27 @@
1
- export function translate(value, locale = "en") {
2
- if (!value) return "";
3
- if (typeof value === "string") return value;
4
- return value[locale] ?? value.en ?? Object.values(value)[0] ?? "";
5
- }
6
- export function formatPrice(amount, currency, locale = "en") {
7
- return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / 100);
8
- }
1
+ import { translate } from "@frigear-nu/ui/translated";
2
+ import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
3
+ export { translate };
4
+ export const DEFAULT_CURRENCY = "DKK";
9
5
  export function normalizeMembership(value) {
10
6
  if (value === "any") return "any";
11
7
  if (Array.isArray(value)) return value.length ? value : "any";
12
8
  return null;
13
9
  }
14
- export function resolveSameOriginUrl(input, origin) {
15
- if (input.startsWith("/")) {
16
- if (input.startsWith("//") || input.startsWith("/\\")) return null;
17
- return `${origin}${input}`;
18
- }
19
- try {
20
- return new URL(input).origin === new URL(origin).origin ? input : null;
21
- } catch {
22
- return null;
23
- }
24
- }
25
10
  export function newId(prefix) {
26
11
  return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
27
12
  }
28
13
  export function newTicketCode() {
29
14
  return `${crypto.randomUUID().replaceAll("-", "")}${crypto.randomUUID().replaceAll("-", "")}`;
30
15
  }
16
+ export function ticketMdcData(type, name, locale = "en") {
17
+ return {
18
+ ticket: name,
19
+ name,
20
+ key: type.key,
21
+ price: type.price === 0 ? "Free" : formatPrice(type.price, type.currency, locale),
22
+ amount: type.price,
23
+ currency: type.currency,
24
+ hidePrice: !!type.hidePrice,
25
+ remaining: type.remaining ?? null
26
+ };
27
+ }
package/dist/types.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ export { type Translated } from '@frigear-nu/ui/translated'
2
+
1
3
  export { default } from './module.mjs'
2
4
 
3
- export { type AddonDto, type AddonGroupDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto, type Translated } from './module.mjs'
5
+ export { type AddonDto, type AddonGroupDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipCheckContext, type MembershipOption, type MembershipOptionsContext, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/events",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Events with ticket types, addons, capacity, membership requirements, Stripe payments and QR check-in for the frigear ecosystem, built on @frigear-nu/payments.",
5
5
  "homepage": "https://docs.frigear.dev/modules/events",
6
6
  "repository": {
@@ -30,12 +30,15 @@
30
30
  "playground"
31
31
  ],
32
32
  "dependencies": {
33
- "@frigear-nu/menu": "0.0.8",
34
- "@frigear-nu/module-kit": "0.1.6",
35
- "@frigear-nu/payments": "0.0.7",
36
- "@frigear-nu/stripe": "0.0.7",
37
- "@frigear-nu/subscriptions": "0.0.7",
38
- "@frigear-nu/users": "1.0.8",
33
+ "@frigear-nu/admin": "0.0.3",
34
+ "@frigear-nu/module-kit": "1.0.0",
35
+ "@frigear-nu/payments": "0.0.9",
36
+ "@frigear-nu/stripe": "0.0.9",
37
+ "@frigear-nu/subscriptions": "0.0.9",
38
+ "@frigear-nu/ui": "0.0.8",
39
+ "@frigear-nu/auth": "0.0.4",
40
+ "@frigear-nu/mcp": "0.0.4",
41
+ "@frigear-nu/users": "1.0.10",
39
42
  "@libsql/client": "^0.18.0",
40
43
  "@nuxt/kit": "^4.6.0",
41
44
  "@vueuse/core": "^15.0.0",
@@ -48,15 +51,25 @@
48
51
  "zod": "^4.6.5"
49
52
  },
50
53
  "peerDependencies": {
54
+ "@frigear-nu/menu": "0.0.10",
55
+ "@frigear-nu/widget": "0.0.3",
51
56
  "@nuxt/ui": "^4.10.0",
52
57
  "@nuxthub/core": "^0.10.8"
53
58
  },
54
59
  "peerDependenciesMeta": {
60
+ "@frigear-nu/menu": {
61
+ "optional": true
62
+ },
63
+ "@frigear-nu/widget": {
64
+ "optional": true
65
+ },
55
66
  "@nuxt/ui": {
56
67
  "optional": true
57
68
  }
58
69
  },
59
70
  "devDependencies": {
71
+ "@frigear-nu/menu": "0.0.10",
72
+ "@frigear-nu/widget": "0.0.3",
60
73
  "@nuxt/devtools": "^3.4.2",
61
74
  "@nuxt/eslint-config": "^1.17.0",
62
75
  "@nuxt/module-builder": "^1.0.3",