@frigear-nu/events 0.0.0-stage → 0.0.2
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/README.md +35 -2
- package/dist/module.d.mts +168 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +113 -0
- package/dist/runtime/app/components/EventCard.d.vue.ts +8 -0
- package/dist/runtime/app/components/EventCard.vue +52 -0
- package/dist/runtime/app/components/EventCard.vue.d.ts +8 -0
- package/dist/runtime/app/components/EventTicketCard.d.vue.ts +12 -0
- package/dist/runtime/app/components/EventTicketCard.vue +82 -0
- package/dist/runtime/app/components/EventTicketCard.vue.d.ts +12 -0
- package/dist/runtime/app/components/EventTicketPicker.d.vue.ts +13 -0
- package/dist/runtime/app/components/EventTicketPicker.vue +133 -0
- package/dist/runtime/app/components/EventTicketPicker.vue.d.ts +13 -0
- package/dist/runtime/app/composables/index.d.ts +1 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useEvents.d.ts +21 -0
- package/dist/runtime/app/composables/useEvents.js +50 -0
- package/dist/runtime/app/pages/event.d.vue.ts +3 -0
- package/dist/runtime/app/pages/event.vue +135 -0
- package/dist/runtime/app/pages/event.vue.d.ts +3 -0
- package/dist/runtime/app/pages/events.d.vue.ts +3 -0
- package/dist/runtime/app/pages/events.vue +43 -0
- package/dist/runtime/app/pages/events.vue.d.ts +3 -0
- package/dist/runtime/app/pages/tickets.d.vue.ts +3 -0
- package/dist/runtime/app/pages/tickets.vue +30 -0
- package/dist/runtime/app/pages/tickets.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +87 -0
- package/dist/runtime/i18n/lang/en.json +87 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +14 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +1095 -0
- package/dist/runtime/server/db/schema.sqlite.js +100 -0
- package/dist/runtime/server/plugins/payment-hooks.d.ts +2 -0
- package/dist/runtime/server/plugins/payment-hooks.js +34 -0
- package/dist/runtime/server/routes/admin/addon-group.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-group.delete.js +7 -0
- package/dist/runtime/server/routes/admin/addon-group.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-group.patch.js +11 -0
- package/dist/runtime/server/routes/admin/addon-groups.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-groups.post.js +11 -0
- package/dist/runtime/server/routes/admin/addon.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon.delete.js +7 -0
- package/dist/runtime/server/routes/admin/addon.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon.patch.js +10 -0
- package/dist/runtime/server/routes/admin/addons.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addons.post.js +12 -0
- package/dist/runtime/server/routes/admin/attendees.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/attendees.get.js +11 -0
- package/dist/runtime/server/routes/admin/event.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.delete.js +10 -0
- package/dist/runtime/server/routes/admin/event.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.patch.js +11 -0
- package/dist/runtime/server/routes/admin/events.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/events.get.js +6 -0
- package/dist/runtime/server/routes/admin/events.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/events.post.js +19 -0
- package/dist/runtime/server/routes/admin/ticket-cancel.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-cancel.post.js +14 -0
- package/dist/runtime/server/routes/admin/ticket-refund.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-refund.post.js +10 -0
- package/dist/runtime/server/routes/admin/ticket-type.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-type.delete.js +10 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +11 -0
- package/dist/runtime/server/routes/admin/ticket-types.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-types.post.js +11 -0
- package/dist/runtime/server/routes/public/check-in.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/check-in.post.js +15 -0
- package/dist/runtime/server/routes/public/event.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/event.get.js +7 -0
- package/dist/runtime/server/routes/public/events.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/events.get.js +1 -0
- package/dist/runtime/server/routes/public/purchase.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/purchase.post.js +12 -0
- package/dist/runtime/server/routes/public/ticket-pay.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/ticket-pay.post.js +10 -0
- package/dist/runtime/server/routes/public/ticket.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/ticket.get.js +4 -0
- package/dist/runtime/server/routes/public/tickets-me.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/tickets-me.get.js +4 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +96 -0
- package/dist/runtime/server/utils/admin-schemas.js +48 -0
- package/dist/runtime/server/utils/admin.d.ts +7 -0
- package/dist/runtime/server/utils/admin.js +24 -0
- package/dist/runtime/server/utils/auth.d.ts +10 -0
- package/dist/runtime/server/utils/auth.js +7 -0
- package/dist/runtime/server/utils/catalog.d.ts +25 -0
- package/dist/runtime/server/utils/catalog.js +129 -0
- package/dist/runtime/server/utils/membership.d.ts +3 -0
- package/dist/runtime/server/utils/membership.js +5 -0
- package/dist/runtime/server/utils/tickets.d.ts +10 -0
- package/dist/runtime/server/utils/tickets.js +122 -0
- package/dist/runtime/utils/index.d.ts +14 -0
- package/dist/runtime/utils/index.js +30 -0
- package/dist/types.d.mts +3 -0
- package/package.json +91 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
2
|
+
export const events = sqliteTable("events", {
|
|
3
|
+
/** `evn_` + random id. */
|
|
4
|
+
id: text().primaryKey(),
|
|
5
|
+
slug: text().notNull().unique(),
|
|
6
|
+
name: text({ mode: "json" }).$type().notNull(),
|
|
7
|
+
description: text({ mode: "json" }).$type(),
|
|
8
|
+
/** Markdown body (per locale). */
|
|
9
|
+
body: text({ mode: "json" }).$type(),
|
|
10
|
+
image: text(),
|
|
11
|
+
location: text(),
|
|
12
|
+
locationUrl: text(),
|
|
13
|
+
startsAt: integer({ mode: "timestamp" }).notNull(),
|
|
14
|
+
endsAt: integer({ mode: "timestamp" }),
|
|
15
|
+
/** `draft | published | archived`. Only published events are public. */
|
|
16
|
+
status: text().notNull().default("draft"),
|
|
17
|
+
/** Same shape as `requiresMembership` on a ticket type; applies to every ticket of the event. */
|
|
18
|
+
requiresMembership: text({ mode: "json" }).$type(),
|
|
19
|
+
createdAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
20
|
+
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
21
|
+
}, (t) => [
|
|
22
|
+
index("event_status_starts_idx").on(t.status, t.startsAt)
|
|
23
|
+
]);
|
|
24
|
+
export const eventTicketTypes = sqliteTable("event_ticket_types", {
|
|
25
|
+
/** `ttp_` + random id. */
|
|
26
|
+
id: text().primaryKey(),
|
|
27
|
+
eventId: text().notNull().references(() => events.id, { onDelete: "cascade" }),
|
|
28
|
+
/** Stable key within the event (e.g. `default`, `premium`). */
|
|
29
|
+
key: text().notNull(),
|
|
30
|
+
name: text({ mode: "json" }).$type().notNull(),
|
|
31
|
+
description: text({ mode: "json" }).$type(),
|
|
32
|
+
/** Price in minor units; `0` is a free ticket. */
|
|
33
|
+
price: integer().notNull().default(0),
|
|
34
|
+
currency: text().notNull().default("DKK"),
|
|
35
|
+
hidePrice: integer({ mode: "boolean" }).notNull().default(false),
|
|
36
|
+
/** Maximum number of sold + reserved tickets; null = unlimited. */
|
|
37
|
+
capacity: integer(),
|
|
38
|
+
saleStartsAt: integer({ mode: "timestamp" }),
|
|
39
|
+
saleEndsAt: integer({ mode: "timestamp" }),
|
|
40
|
+
maxPerUser: integer().notNull().default(1),
|
|
41
|
+
/** `null` = open to everyone, `'any'` = any active subscription, or a list of plan keys. */
|
|
42
|
+
requiresMembership: text({ mode: "json" }).$type(),
|
|
43
|
+
isDefault: integer({ mode: "boolean" }).notNull().default(false),
|
|
44
|
+
sortOrder: integer().notNull().default(0),
|
|
45
|
+
createdAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
46
|
+
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
47
|
+
}, (t) => [
|
|
48
|
+
uniqueIndex("event_ticket_type_key_idx").on(t.eventId, t.key)
|
|
49
|
+
]);
|
|
50
|
+
export const eventAddonGroups = sqliteTable("event_addon_groups", {
|
|
51
|
+
/** `agr_` + random id. */
|
|
52
|
+
id: text().primaryKey(),
|
|
53
|
+
ticketTypeId: text().notNull().references(() => eventTicketTypes.id, { onDelete: "cascade" }),
|
|
54
|
+
title: text({ mode: "json" }).$type().notNull(),
|
|
55
|
+
/** When true, at least one addon of the group must be chosen. */
|
|
56
|
+
required: integer({ mode: "boolean" }).notNull().default(false),
|
|
57
|
+
/** When false (the default) at most one addon of the group can be chosen. */
|
|
58
|
+
multiple: integer({ mode: "boolean" }).notNull().default(false),
|
|
59
|
+
sortOrder: integer().notNull().default(0)
|
|
60
|
+
}, (t) => [
|
|
61
|
+
index("event_addon_group_ticket_idx").on(t.ticketTypeId)
|
|
62
|
+
]);
|
|
63
|
+
export const eventAddons = sqliteTable("event_addons", {
|
|
64
|
+
/** `adn_` + random id. */
|
|
65
|
+
id: text().primaryKey(),
|
|
66
|
+
groupId: text().notNull().references(() => eventAddonGroups.id, { onDelete: "cascade" }),
|
|
67
|
+
label: text({ mode: "json" }).$type().notNull(),
|
|
68
|
+
description: text({ mode: "json" }).$type(),
|
|
69
|
+
/** Price in minor units, in the ticket type's currency; `0` is included in the ticket. */
|
|
70
|
+
price: integer().notNull().default(0),
|
|
71
|
+
/** Maximum number of sold + reserved addons; null = unlimited. */
|
|
72
|
+
capacity: integer(),
|
|
73
|
+
sortOrder: integer().notNull().default(0)
|
|
74
|
+
}, (t) => [
|
|
75
|
+
index("event_addon_group_idx").on(t.groupId)
|
|
76
|
+
]);
|
|
77
|
+
export const eventTickets = sqliteTable("event_tickets", {
|
|
78
|
+
/** `tkt_` + random id. */
|
|
79
|
+
id: text().primaryKey(),
|
|
80
|
+
eventId: text().notNull(),
|
|
81
|
+
ticketTypeId: text().notNull(),
|
|
82
|
+
userId: integer().notNull(),
|
|
83
|
+
/** The payment that covers it; null for free tickets. */
|
|
84
|
+
paymentId: text(),
|
|
85
|
+
/** `pending | paid | cancelled | refunded | expired`. `pending` and `paid` hold a seat. */
|
|
86
|
+
status: text().notNull().default("pending"),
|
|
87
|
+
addons: text({ mode: "json" }).$type().notNull(),
|
|
88
|
+
amount: integer().notNull(),
|
|
89
|
+
currency: text().notNull(),
|
|
90
|
+
/** Unguessable value shown as a QR code and scanned at check-in. */
|
|
91
|
+
code: text().notNull().unique(),
|
|
92
|
+
checkedInAt: integer({ mode: "timestamp" }),
|
|
93
|
+
checkedInBy: integer(),
|
|
94
|
+
createdAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
95
|
+
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
96
|
+
}, (t) => [
|
|
97
|
+
index("event_ticket_event_idx").on(t.eventId, t.status),
|
|
98
|
+
index("event_ticket_user_idx").on(t.userId),
|
|
99
|
+
index("event_ticket_payment_idx").on(t.paymentId)
|
|
100
|
+
]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
3
|
+
import { toTicketDtos } from "../utils/catalog.js";
|
|
4
|
+
export default defineNitroPlugin((nitro) => {
|
|
5
|
+
const ticketOf = async ({ payment }) => {
|
|
6
|
+
if (payment.referenceType !== "event_ticket" || !payment.referenceId) return null;
|
|
7
|
+
const [row] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, payment.referenceId)).limit(1);
|
|
8
|
+
return row ?? null;
|
|
9
|
+
};
|
|
10
|
+
nitro.hooks.hook("payments:paid", async (ctx) => {
|
|
11
|
+
const ticket = await ticketOf(ctx);
|
|
12
|
+
if (!ticket || ticket.status === "paid") return;
|
|
13
|
+
const [updated] = await db.update(schema.eventTickets).set({ status: "paid", paymentId: ctx.payment.id }).where(and(eq(schema.eventTickets.id, ticket.id), inArray(schema.eventTickets.status, ["pending", "expired", "cancelled"]))).returning();
|
|
14
|
+
if (!updated) return;
|
|
15
|
+
const [dto] = await toTicketDtos([updated]);
|
|
16
|
+
await nitro.hooks.callHook("events:ticket:paid", { ticket: dto });
|
|
17
|
+
});
|
|
18
|
+
const release = (status) => async (ctx) => {
|
|
19
|
+
const ticket = await ticketOf(ctx);
|
|
20
|
+
if (!ticket) return;
|
|
21
|
+
await db.update(schema.eventTickets).set({ status }).where(and(eq(schema.eventTickets.id, ticket.id), eq(schema.eventTickets.status, "pending")));
|
|
22
|
+
};
|
|
23
|
+
nitro.hooks.hook("payments:expired", release("expired"));
|
|
24
|
+
nitro.hooks.hook("payments:failed", release("expired"));
|
|
25
|
+
nitro.hooks.hook("payments:refunded", async (ctx) => {
|
|
26
|
+
if (ctx.payment.status !== "refunded") return;
|
|
27
|
+
const ticket = await ticketOf(ctx);
|
|
28
|
+
if (!ticket || ticket.status === "refunded") return;
|
|
29
|
+
const [updated] = await db.update(schema.eventTickets).set({ status: "refunded" }).where(eq(schema.eventTickets.id, ticket.id)).returning();
|
|
30
|
+
if (!updated) return;
|
|
31
|
+
const [dto] = await toTicketDtos([updated]);
|
|
32
|
+
await nitro.hooks.callHook("events:ticket:refunded", { ticket: dto });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
await db.delete(schema.eventAddonGroups).where(eq(schema.eventAddonGroups.id, getRouterParam(event, "id")));
|
|
6
|
+
return { ok: true };
|
|
7
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { addonGroupInput } from "../../utils/admin-schemas.js";
|
|
4
|
+
const patchSchema = addonGroupInput.omit({ addons: true }).partial();
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
await requireEventsManager(event);
|
|
7
|
+
const patch = await readValidatedBody(event, patchSchema.parse);
|
|
8
|
+
const [row] = await db.update(schema.eventAddonGroups).set(patch).where(eq(schema.eventAddonGroups.id, getRouterParam(event, "id"))).returning();
|
|
9
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Addon group not found." });
|
|
10
|
+
return row;
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { insertAddonGroup } from "../../utils/admin.js";
|
|
2
|
+
import { addonGroupInput } from "../../utils/admin-schemas.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const input = await readValidatedBody(event, addonGroupInput.parse);
|
|
6
|
+
const row = await insertAddonGroup(getRouterParam(event, "id"), input).catch((error) => {
|
|
7
|
+
throw createError({ statusCode: 409, statusMessage: "Could not create the addon group (unknown ticket type?).", cause: error });
|
|
8
|
+
});
|
|
9
|
+
setResponseStatus(event, 201);
|
|
10
|
+
return row;
|
|
11
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
await db.delete(schema.eventAddons).where(eq(schema.eventAddons.id, getRouterParam(event, "id")));
|
|
6
|
+
return { ok: true };
|
|
7
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { addonInput } from "../../utils/admin-schemas.js";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await requireEventsManager(event);
|
|
6
|
+
const patch = await readValidatedBody(event, addonInput.partial().parse);
|
|
7
|
+
const [row] = await db.update(schema.eventAddons).set(patch).where(eq(schema.eventAddons.id, getRouterParam(event, "id"))).returning();
|
|
8
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Addon not found." });
|
|
9
|
+
return row;
|
|
10
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { newId } from "../../../utils/index.js";
|
|
3
|
+
import { addonInput } from "../../utils/admin-schemas.js";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await requireEventsManager(event);
|
|
6
|
+
const input = await readValidatedBody(event, addonInput.parse);
|
|
7
|
+
const row = await db.insert(schema.eventAddons).values({ id: newId("adn"), groupId: getRouterParam(event, "id"), ...input, description: input.description ?? null, capacity: input.capacity ?? null }).returning().catch((error) => {
|
|
8
|
+
throw createError({ statusCode: 409, statusMessage: "Could not create the addon (unknown group?).", cause: error });
|
|
9
|
+
});
|
|
10
|
+
setResponseStatus(event, 201);
|
|
11
|
+
return row[0];
|
|
12
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { asc, eq } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const rows = await db.select({
|
|
6
|
+
ticket: schema.eventTickets,
|
|
7
|
+
userName: schema.users.name,
|
|
8
|
+
userEmail: schema.users.email
|
|
9
|
+
}).from(schema.eventTickets).leftJoin(schema.users, eq(schema.eventTickets.userId, schema.users.id)).where(eq(schema.eventTickets.eventId, getRouterParam(event, "id"))).orderBy(asc(schema.eventTickets.createdAt));
|
|
10
|
+
return rows.map(({ ticket, userName, userEmail }) => ({ ...ticket, userName, userEmail }));
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const id = getRouterParam(event, "id");
|
|
6
|
+
const [sold] = await db.select({ count: sql`count(*)` }).from(schema.eventTickets).where(and(eq(schema.eventTickets.eventId, id), eq(schema.eventTickets.status, "paid")));
|
|
7
|
+
if (Number(sold?.count ?? 0) > 0) throw createError({ statusCode: 409, statusMessage: "This event has paid tickets. Archive it instead." });
|
|
8
|
+
await db.delete(schema.events).where(eq(schema.events.id, id));
|
|
9
|
+
return { ok: true };
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
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();
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
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;
|
|
11
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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";
|
|
5
|
+
import { eventInput } from "../../utils/admin-schemas.js";
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
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);
|
|
17
|
+
setResponseStatus(event, 201);
|
|
18
|
+
return row;
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const id = getRouterParam(event, "id");
|
|
6
|
+
const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, id)).limit(1);
|
|
7
|
+
if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
|
|
8
|
+
if (ticket.status === "paid" && ticket.amount > 0) {
|
|
9
|
+
throw createError({ statusCode: 409, statusMessage: "This ticket was paid for. Refund it instead." });
|
|
10
|
+
}
|
|
11
|
+
const [updated] = await db.update(schema.eventTickets).set({ status: "cancelled" }).where(and(eq(schema.eventTickets.id, id), inArray(schema.eventTickets.status, ["pending", "paid"]))).returning();
|
|
12
|
+
if (!updated) throw createError({ statusCode: 409, statusMessage: `Ticket is already ${ticket.status}.` });
|
|
13
|
+
return { ok: true };
|
|
14
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, getRouterParam(event, "id"))).limit(1);
|
|
6
|
+
if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
|
|
7
|
+
if (ticket.status !== "paid" || !ticket.paymentId) throw createError({ statusCode: 409, statusMessage: "Only paid tickets with a payment can be refunded." });
|
|
8
|
+
await refundPayment(ticket.paymentId);
|
|
9
|
+
return { ok: true };
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const id = getRouterParam(event, "id");
|
|
6
|
+
const [sold] = await db.select({ count: sql`count(*)` }).from(schema.eventTickets).where(and(eq(schema.eventTickets.ticketTypeId, id), inArray(schema.eventTickets.status, ["paid", "pending"])));
|
|
7
|
+
if (Number(sold?.count ?? 0) > 0) throw createError({ statusCode: 409, statusMessage: "This ticket type has tickets. Cancel them first." });
|
|
8
|
+
await db.delete(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.id, id));
|
|
9
|
+
return { ok: true };
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
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();
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
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;
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { insertTicketType } from "../../utils/admin.js";
|
|
2
|
+
import { ticketTypeInput } from "../../utils/admin-schemas.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireEventsManager(event);
|
|
5
|
+
const input = await readValidatedBody(event, ticketTypeInput.parse);
|
|
6
|
+
const row = await insertTicketType(getRouterParam(event, "id"), input).catch((error) => {
|
|
7
|
+
throw createError({ statusCode: 409, statusMessage: "Could not create the ticket type (unknown event, or the key is taken).", cause: error });
|
|
8
|
+
});
|
|
9
|
+
setResponseStatus(event, 201);
|
|
10
|
+
return row;
|
|
11
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const bodySchema = z.object({ code: z.string().min(1) });
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const { userId } = await requireEventsManager(event);
|
|
7
|
+
const { code } = await readValidatedBody(event, bodySchema.parse);
|
|
8
|
+
const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.code, code)).limit(1);
|
|
9
|
+
if (!ticket) throw createError({ statusCode: 404, statusMessage: "Unknown ticket code." });
|
|
10
|
+
if (ticket.status !== "paid") throw createError({ statusCode: 409, statusMessage: `This ticket is ${ticket.status}, not valid for entry.` });
|
|
11
|
+
const [claimed] = await db.update(schema.eventTickets).set({ checkedInAt: /* @__PURE__ */ new Date(), checkedInBy: userId }).where(and(eq(schema.eventTickets.id, ticket.id), isNull(schema.eventTickets.checkedInAt))).returning();
|
|
12
|
+
const row = claimed ?? (await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, ticket.id)).limit(1))[0];
|
|
13
|
+
const [dto] = await toTicketDtos([row]);
|
|
14
|
+
return { ok: true, alreadyCheckedIn: !claimed, checkedInAt: row.checkedInAt.toISOString(), ticket: { ...dto, code: null } };
|
|
15
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
const slug = getRouterParam(event, "slug");
|
|
3
|
+
const row = await getPublishedEventRow(slug);
|
|
4
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
|
|
5
|
+
const session = await getUserSession(event);
|
|
6
|
+
return getEventDetail(row, session.user?.id);
|
|
7
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default defineEventHandler(() => listPublishedEvents());
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const bodySchema = z.object({
|
|
3
|
+
ticketTypeId: z.string().min(1),
|
|
4
|
+
addonIds: z.array(z.string().min(1)).max(50).optional(),
|
|
5
|
+
successUrl: z.string().optional(),
|
|
6
|
+
cancelUrl: z.string().optional()
|
|
7
|
+
});
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const { user } = await requireUserSession(event);
|
|
10
|
+
const body = await readValidatedBody(event, bodySchema.parse);
|
|
11
|
+
return purchaseTicket(event, user.id, getRouterParam(event, "slug"), body);
|
|
12
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const { user } = await requireUserSession(event);
|
|
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
|
+
if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
|
|
7
|
+
if (ticket.status === "paid") throw createError({ statusCode: 409, statusMessage: "This ticket is already paid." });
|
|
8
|
+
if (ticket.status !== "pending" || !ticket.paymentId) throw createError({ statusCode: 409, statusMessage: "This ticket can no longer be paid. Buy a new one." });
|
|
9
|
+
return resumePayment(ticket.paymentId, user.id, event);
|
|
10
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const translated: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
3
|
+
export declare const membership: z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>;
|
|
4
|
+
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
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
9
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const addonGroupInput: z.ZodObject<{
|
|
12
|
+
title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
13
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
multiple: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
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>]>>>;
|
|
19
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
21
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
}, z.core.$strip>>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export declare const ticketTypeInput: z.ZodObject<{
|
|
25
|
+
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>]>>>;
|
|
28
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
currency: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
30
|
+
hidePrice: z.ZodOptional<z.ZodBoolean>;
|
|
31
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
32
|
+
saleStartsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
33
|
+
saleEndsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
34
|
+
maxPerUser: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
|
|
36
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
addonGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
39
|
+
title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
40
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
41
|
+
multiple: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
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>]>>>;
|
|
46
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
48
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
}, z.core.$strip>>>;
|
|
50
|
+
}, z.core.$strip>>>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export declare const eventInput: z.ZodObject<{
|
|
53
|
+
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>]>>>;
|
|
57
|
+
image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
59
|
+
locationUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
60
|
+
startsAt: z.ZodCoercedDate<unknown>;
|
|
61
|
+
endsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
62
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
63
|
+
draft: "draft";
|
|
64
|
+
published: "published";
|
|
65
|
+
archived: "archived";
|
|
66
|
+
}>>;
|
|
67
|
+
requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
|
|
68
|
+
ticketTypes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
69
|
+
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>]>>>;
|
|
72
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
currency: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
74
|
+
hidePrice: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
76
|
+
saleStartsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
77
|
+
saleEndsAt: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
78
|
+
maxPerUser: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
requiresMembership: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>;
|
|
80
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
addonGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
83
|
+
title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
84
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
85
|
+
multiple: z.ZodOptional<z.ZodBoolean>;
|
|
86
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
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>]>>>;
|
|
90
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
92
|
+
sortOrder: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
}, z.core.$strip>>>;
|
|
94
|
+
}, z.core.$strip>>>;
|
|
95
|
+
}, z.core.$strip>>>;
|
|
96
|
+
}, z.core.$strip>;
|