Skip to content

Commit

Permalink
Merge pull request #65 from mustachebash/update/tier-rolling
Browse files Browse the repository at this point in the history
enabled automatic archiving and optional tier rolling for products with max quantities
  • Loading branch information
jfurfaro authored Jan 19, 2024
2 parents a6eedb8 + 21a1fb3 commit e7fa3d8
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 35 deletions.
1 change: 1 addition & 0 deletions lib/routes/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ ordersRouter
return ctx.body = {confirmationId: transaction.processorTransactionId, orderId: order.id, token: orderToken};
} catch(e) {
if(e.code === 'INVALID') ctx.throw(400, e, {expose: false});
if(e.code === 'GONE') ctx.throw(410, e, {expose: false});

ctx.throw(e);
}
Expand Down
121 changes: 86 additions & 35 deletions lib/services/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,17 @@ module.exports = {
if (!cart.length || !paymentMethodNonce || !customer.firstName || !customer.lastName || !customer.email) throw new OrdersServiceError('Invalid payment parameters', 'INVALID');

const products = (await sql`
SELECT *
FROM products
WHERE id in ${sql(cart.map(i => i.productId))}
SELECT p.*, COALESCE(SUM(oi.quantity), 0) as total_sold
FROM products as p
LEFT JOIN order_items as oi
ON p.id = oi.product_id
WHERE p.id in ${sql(cart.map(i => i.productId))}
AND status = 'active'
GROUP BY 1
`).map(p => ({
...p,
...(typeof p.price === 'string' ? {price: Number(p.price)} : {})
...(typeof p.price === 'string' ? {price: Number(p.price)} : {}),
...(typeof p.totalSold === 'string' ? {totalSold: Number(p.totalSold)} : {})
}));

let promo;
Expand All @@ -151,50 +155,92 @@ module.exports = {
WHERE id = ${promoId}
`).map(p => ({
...p,
...(typeof p.price === 'string' ? {price: Number(p.price)} : {})
...(typeof p.price === 'string' ? {price: Number(p.price)} : {}),
...(typeof p.percentDiscount === 'string' ? {percentDiscount: Number(p.percentDiscount)} : {}),
...(typeof p.flatDiscount === 'string' ? {flatDiscount: Number(p.flatDiscount)} : {})
}));

if(promo.status !== 'active') throw new OrdersServiceError('Invalid promo code', 'INVALID');
}

// Ensure we have the all the products attempting to be purchased
if(!products.length || products.length !== cart.length) throw new OrdersServiceError('Empty/Invalid items in cart', 'INVALID');
// For now, use a slug of `GONE` since this should only occur when a product has become inactive since page load
// if(!products.length || products.length !== cart.length) throw new OrdersServiceError('Empty/Invalid items in cart', 'INVALID');
if(!products.length || products.length !== cart.length) throw new OrdersServiceError('Unavailable items in cart', 'GONE');

const productsToArchive = [];
const orderDetails = cart.map(i => {
// TODO: reimplement this with a live query for current totals
// const product = products.find(p => p.id === i.productId),
// remaining = typeof product.quantity === 'number' && product.quantity > 0 ? product.quantity - (product.sold || 0) : null;
const product = products.find(p => p.id === i.productId);

// if(remaining !== null) {
// if(remaining < i.quantity) throw new OrdersServiceError('Purchase exceeds remaining quantity', 'INVALID');

// const totalSold = (product.sold || 0) + i.quantity;
// productsToUpdate.push({
// id: product.id,
// sold: totalSold,
// // Mark as archived if this order sells the item out
// status: totalSold >= product.quantity ? 'archived' : product.status
// });
// }
const product = products.find(p => p.id === i.productId),
remaining = typeof product.maxQuantity === 'number' && product.maxQuantity > 0 ? product.maxQuantity - product.totalSold : null;
// const product = products.find(p => p.id === i.productId);

if(remaining !== null) {
// So long as the product was active at start, don't worry if this individual order goes over the max
// This minimizes the amount of customers who are buying and get failed orders
// if(remaining < i.quantity) throw new OrdersServiceError('Purchase exceeds remaining quantity', 'INVALID');

const totalSold = product.totalSold + i.quantity;

if(totalSold >= product.maxQuantity) {
productsToArchive.push({
id: product.id,
eventId: product.eventId,
nextTierProductId: product.meta.nextTierProductId ?? null
});
}
}

return {
...i,
product
};
});

// Do some janky non-transactional tier rolling for now
try {
for(const pta of productsToArchive) {
await sql`
UPDATE products
SET status = 'archived', updated = now()
WHERE id = ${pta.id}
`;

if(pta.nextTierProductId) {
await sql`
UPDATE products
SET status = 'active', updated = now()
WHERE id = ${pta.nextTierProductId}
`;

await sql`
UPDATE events
SET meta = jsonb_set(meta, '{currentTicket}', ${pta.nextTierProductId})
WHERE id = ${pta.eventId}
`;
}
}
} catch(e) {
log.error(e, 'Failed to archive sold out products');
}

const productSubtotal = orderDetails.map(i => Number(i.quantity) * i.product.price).reduce((tot, cur) => tot + cur, 0);

// We don't sell things for free - if this is 0 and there's no promo, there's a bad purchase attempt
if(productSubtotal === 0 && (!promo || !promo.price)) throw new OrdersServiceError('Empty/Invalid items in cart', 'INVALID');

let amount = productSubtotal;

// Overwrite amount if there's a promo and it sets a price
// Include the service fee and any donation amount
if(promo && promo.price && promo.quantity) {
amount = (promo.price * promo.quantity);
// Overwrite/adjust amount if there's a promo
if(promo) {
if(promo.price) {
amount = (promo.price);
}

if(promo.percentDiscount) {
amount = amount - (amount * (promo.percentDiscount / 100));
// Round to 2 decimal places
amount = Math.round(amount * 100) / 100;
}
}

// Find or insert a customer record immediately before attempting charge
Expand Down Expand Up @@ -285,15 +331,20 @@ module.exports = {
}

// Write the order to the DB (transaction needed?)
await sql`
INSERT INTO orders ${sql(order)}
`;
await sql`
INSERT INTO order_items ${sql(orderItems)}
`;
await sql`
INSERT INTO transactions ${sql(transaction)}
`;
try {
await sql`
INSERT INTO orders ${sql(order)}
`;
await sql`
INSERT INTO order_items ${sql(orderItems)}
`;
await sql`
INSERT INTO transactions ${sql(transaction)}
`;
} catch(e) {
// Don't let this write fail the response - the customer has been charged at this point
log.error(e, 'Error writing order/order items/transactions to DB');
}

// Write a guest with the purchaser's name to the DB
orderDetails.forEach(i => {
Expand Down

0 comments on commit e7fa3d8

Please sign in to comment.