Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/routes/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Router from '@koa/router';
import { AppContext } from '../index.js';
import { authorizeUser, requiresPermission } from '../middleware/auth.js';
import { createCustomer, getCustomers, getCustomer, updateCustomer } from '../services/customers.js';
import { isRecordLike } from '../utils/type-guards.js';

const customersRouter = new Router<AppContext['state'], AppContext>({
prefix: '/customers'
Expand All @@ -18,6 +19,8 @@ customersRouter
}
})
.post('/', authorizeUser, requiresPermission('admin'), async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const customer = await createCustomer({...ctx.request.body, createdBy: ctx.state.user.id});

Expand Down
5 changes: 5 additions & 0 deletions lib/routes/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Router from '@koa/router';
import { authorizeUser, requiresPermission } from '../middleware/auth.js';
import { getEvents, getEvent, createEvent, updateEvent, getEventSummary, getOpeningSales, getEventExtendedStats, getEventDailyTickets, getEventCheckins } from '../services/events.js';
import { isRecordLike } from '../utils/type-guards.js';

const eventsRouter = new Router({
prefix: '/events'
Expand All @@ -19,6 +20,8 @@ eventsRouter
}
})
.post('/', requiresPermission('admin'), async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const event = await createEvent(ctx.request.body);

Expand All @@ -43,6 +46,8 @@ eventsRouter
}
})
.patch('/:id', requiresPermission('admin'), async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const event = await updateEvent(ctx.params.id, {...ctx.request.body, updatedBy: ctx.state.user.id});

Expand Down
5 changes: 5 additions & 0 deletions lib/routes/guests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
updateGuest,
archiveGuest
} from '../services/guests.js';
import { isRecordLike } from '../utils/type-guards.js';

const guestsRouter = new Router({
prefix: '/guests'
Expand All @@ -25,6 +26,8 @@ guestsRouter
}
})
.post('/', requiresPermission('write'), async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const guest = await createGuest({...ctx.request.body, createdBy: ctx.state.user.id, createdReason: 'comp'});

Expand All @@ -51,6 +54,8 @@ guestsRouter
}
})
.patch('/:id', async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const guest = await updateGuest(ctx.params.id, {updatedBy: ctx.state.user.id, ...ctx.request.body});

Expand Down
5 changes: 3 additions & 2 deletions lib/routes/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getOrderTickets, transferTickets } from '../services/tickets.js';
import { createOrder, getOrders, getOrder, getOrderTransfers, refundOrder, generateOrderToken } from '../services/orders.js';
import { sendReceipt, upsertEmailSubscriber, sendTransfereeConfirmation, sendUpgradeReceipt } from '../services/email.js';
import { getTransactions } from '../services/transactions.js';
import { isRecordLike } from '../utils/type-guards.js';

// TODO: make this configurable at some point
const EMAIL_LIST = '90392ecd5e',
Expand All @@ -24,7 +25,7 @@ ordersRouter
}
})
.post('/', async ctx => {
if(!ctx.request.body) throw ctx.throw(400);
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const { order, transaction, customer } = await createOrder({...ctx.request.body}),
Expand Down Expand Up @@ -136,7 +137,7 @@ ordersRouter
}
})
.post('/:id/transfers', authorizeUser, requiresPermission('write'), async ctx => {
if(!ctx.request.body) throw ctx.throw(400);
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const { transferee, order } = await transferTickets(ctx.params.id, ctx.request.body, ctx.state.user.id),
Expand Down
5 changes: 5 additions & 0 deletions lib/routes/products.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Router from '@koa/router';
import { authorizeUser } from '../middleware/auth.js';
import { createProduct, getProducts, getProduct, updateProduct } from '../services/products.js';
import { isRecordLike } from '../utils/type-guards.js';

const productsRouter = new Router({
prefix: '/products'
Expand All @@ -19,6 +20,8 @@ productsRouter
}
})
.post('/', async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const product = await createProduct(ctx.request.body);

Expand All @@ -45,6 +48,8 @@ productsRouter
}
})
.patch('/:id', async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const product = await updateProduct(ctx.params.id, {...ctx.request.body, updatedBy: ctx.state.user.id});

Expand Down
3 changes: 3 additions & 0 deletions lib/routes/promos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Router from '@koa/router';
import { authorizeUser, requiresPermission } from '../middleware/auth.js';
import { createPromo, getPromos, getPromo, updatePromo } from '../services/promos.js';
import { getProduct } from '../services/products.js';
import { isRecordLike } from '../utils/type-guards.js';

const promosRouter = new Router({
prefix: '/promos'
Expand All @@ -18,6 +19,8 @@ promosRouter
}
})
.post('/', authorizeUser, requiresPermission('write'), async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);

try {
const promo = await createPromo({...ctx.request.body, createdBy: ctx.state.user.id});

Expand Down
2 changes: 2 additions & 0 deletions lib/routes/sites.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Router from '@koa/router';
import { upsertEmailSubscriber } from '../services/email.js';
import { isRecordLike } from '../utils/type-guards.js';

// TODO: make this configurable at some point
const EMAIL_LIST_ID = '90392ecd5e';
Expand All @@ -11,6 +12,7 @@ const sitesRouter = new Router({
// Keeping this unti the front end is updated to hit a different route
sitesRouter
.post('/:id/mailing-list', async ctx => {
if(!isRecordLike(ctx.request.body)) throw ctx.throw(400);
if(!ctx.request.body.email || !ctx.request.body.firstName || !ctx.request.body.lastName) throw ctx.throw(400);

try {
Expand Down
45 changes: 31 additions & 14 deletions lib/services/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class CustomerServiceError extends Error {
code: string;
context?: unknown;

constructor(message = 'An unknown error occured', code = 'UNKNOWN', context) {
constructor(message = 'An unknown error occured', code = 'UNKNOWN', context?: unknown) {
super(message);

this.name = this.constructor.name;
Expand All @@ -21,6 +21,17 @@ export class CustomerServiceError extends Error {
}
}

export type Customer = {
id: string;
email: string;
firstName: string;
lastName: string;
created: Date;
updated: Date;
updatedBy: string | null;
meta: Record<string, unknown>;
};

const customerColumns = [
'id',
'email',
Expand All @@ -32,8 +43,14 @@ const customerColumns = [
'meta'
];

type CustomerInput = Record<string, unknown> & {
firstName?: string;
lastName?: string;
email?: string;
meta?: Record<string, unknown>;
};

export async function createCustomer({ firstName, lastName, email, meta }) {
export async function createCustomer({ firstName, lastName, email, meta }: CustomerInput) {
if(!firstName || !lastName || !email) throw new CustomerServiceError('Missing customer data', 'INVALID');
if(!/.+@.+\..{2,}/.test(email)) throw new CustomerServiceError('Invalid email', 'INVALID');

Expand All @@ -48,20 +65,20 @@ export async function createCustomer({ firstName, lastName, email, meta }) {
};

try {
const [createdCustomer] = (await sql`
const [createdCustomer] = await sql<Customer[]>`
INSERT INTO customers ${sql(customer)}
RETURNING ${sql(customerColumns)}
`);
`;

return createdCustomer;
} catch(e) {
throw new CustomerServiceError('Could not create customer', 'UNKNOWN', e);
}
}

export async function getCustomers() {
export async function getCustomers(_options?: Record<string, unknown>): Promise<Customer[]> {
try {
const customers = await sql`
const customers = await sql<Customer[]>`
SELECT ${sql(customerColumns)}
FROM customers
`;
Expand All @@ -72,14 +89,14 @@ export async function getCustomers() {
}
}

export async function getCustomer(id) {
let customer;
export async function getCustomer(id: string): Promise<Customer> {
let customer: Customer | undefined;
try {
[customer] = (await sql`
[customer] = await sql<Customer[]>`
SELECT ${sql(customerColumns)}
FROM customers
WHERE id = ${id}
`);
`;
} catch(e) {
throw new CustomerServiceError('Could not query customers', 'UNKNOWN', e);
}
Expand All @@ -89,7 +106,7 @@ export async function getCustomer(id) {
return customer;
}

export async function updateCustomer(id, updates) {
export async function updateCustomer(id: string, updates: Record<string, unknown>): Promise<Customer> {
for(const u in updates) {
// Update whitelist
if(![
Expand All @@ -103,14 +120,14 @@ export async function updateCustomer(id, updates) {

if(Object.keys(updates).length === 1 && updates.updatedBy) throw new CustomerServiceError('Invalid customer data', 'INVALID');

let customer;
let customer: Customer | undefined;
try {
[customer] = (await sql`
[customer] = await sql<Customer[]>`
UPDATE customers
SET ${sql(updates)}, updated = now()
WHERE id = ${id}
RETURNING ${sql(customerColumns)}
`);
`;
} catch(e) {
throw new CustomerServiceError('Could not update customer', 'UNKNOWN', e);
}
Expand Down
8 changes: 4 additions & 4 deletions lib/services/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import MailChimpClient from 'mailchimp-api-v3';
const mailgun = MailgunJs({apiKey: config.mailgun.apiKey, domain: config.mailgun.domain});

const mailchimp = new MailChimpClient(config.mailchimp.apiKey),
md5 = string => crypto.createHash('md5').update(string).digest('hex');
md5 = (string: string) => crypto.createHash('md5').update(string).digest('hex');

/* eslint-disable max-len */
/**
Expand All @@ -25,7 +25,7 @@ const mailchimp = new MailChimpClient(config.mailchimp.apiKey),
* @param {Array} [tags=[] string }] tags to apply to member
* @return {Promise}
*/
export async function upsertEmailSubscriber(listId, { email, firstName, lastName, tags = [] }) {
export async function upsertEmailSubscriber(listId: string, { email, firstName, lastName, tags = [] }: { email: string; firstName: string; lastName: string; tags?: string[] }) {
const memberHash = md5(email.toLowerCase());

try {
Expand All @@ -50,7 +50,7 @@ export async function upsertEmailSubscriber(listId, { email, firstName, lastName
}
}

export function sendReceipt(guestFirstName, guestLastName, guestEmail, confirmation, orderId, orderToken, amount) {
export function sendReceipt(guestFirstName: string, guestLastName: string, guestEmail: string, confirmation: string, orderId: string, orderToken: string, amount: number) {
mailgun.messages().send({
from: 'Mustache Bash Tickets <contact@mustachebash.com>',
to: guestFirstName + ' ' + guestLastName + ' <' + guestEmail + '> ',
Expand Down Expand Up @@ -360,7 +360,7 @@ table[class=body] .article {
.catch(err => log.error({err, customerEmail, confirmation}, 'Receipt email failed to send'));
}

export function sendTransfereeConfirmation(transfereeFirstName, transfereeLastName, transfereeEmail, parentOrderId, orderToken) {
export function sendTransfereeConfirmation(transfereeFirstName: string, transfereeLastName: string, transfereeEmail: string, parentOrderId: string, orderToken: string) {
mailgun.messages().send({
from: 'Mustache Bash Tickets <contact@mustachebash.com>',
to: transfereeFirstName + ' ' + transfereeLastName + ' <' + transfereeEmail + '> ',
Expand Down
Loading
Loading