Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(merch): Config to use mongodb #167

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions apps/merch/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ ORDER_TABLE_NAME=
ORDER_HOLD_TABLE_NAME=
CORS_ORIGIN=http://localhost:3001
STRIPE_SECRET_KEY=
MONGODB_URI=
1 change: 1 addition & 0 deletions apps/merch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dotenv": "^16.3.1",
"express": "^4.19.2",
"merch-helpers": "*",
"mongoose": "^8.6.1",
"nodelogger": "*",
"stripe": "^12.5.0",
"trpc-panel": "^1.3.4",
Expand Down
58 changes: 58 additions & 0 deletions apps/merch/src/db/mongodb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import mongoose, { Model, Document, FilterQuery } from "mongoose";
import { Logger } from "nodelogger";

export class NotFoundError {
message: string;
constructor(item = "") {
this.message = "Item " + item + " not found.";
}
}

// readTable scans the table for all entries
export const readTable = async<T extends Document>(model: Model<T>): Promise<T[]> => {
const response = await model.find().exec();
if (!response) {
return [];
}
return response as T[];
};

// readItem retrieves the specified item from the table.
export const readItem = async <T extends Document>(
model: Model<T>,
key: string,
keyId = "id"
): Promise<T> => {
const query: FilterQuery<T> = { [keyId]: key } as FilterQuery<T>;
const response = await model.findOne(query).exec();
if (!response) {
Logger.warn(`Item does not exist in table ${model.modelName}`);
throw new NotFoundError(key);
}
return response as T;
};

// writeItem adds the given item to the specified model table.
export const writeItem = async <T extends Document>(
model: Model<T>,
item: T
): Promise<T | null> => {
try {
return await model.create(item);
} catch (error) {
const errorMessage = (error as Error).message
Logger.error(`An error occurred: ${errorMessage}`);
return null;
}
};

export const setupDb = async (): Promise<void> => {
try {
const mongoDb = process.env.MONGODB_URI ?? "";
await mongoose.connect(mongoDb);
Logger.info("Database connected successfully");
} catch (error) {
const errorMessage = (error as Error).message
Logger.error(`Database connection error: ${errorMessage}`);
}
};
121 changes: 54 additions & 67 deletions apps/merch/src/db/orders.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,94 @@
import { readItem, writeItem } from "./dynamodb";
import { v4 as uuidv4 } from "uuid";
import { Order, OrderItem, OrderStatus, OrderHoldEntry } from "types";
import { readItem, readTable, writeItem } from "./mongodb";
import { Order, OrderItem, OrderStatus } from "types";
import { OrderDocument, OrderModel } from "../models/Order";

const ORDER_TABLE_NAME = process.env.ORDER_TABLE_NAME;
const ORDER_HOLD_TABLE_NAME = process.env.ORDER_HOLD_TABLE_NAME;

if (!ORDER_TABLE_NAME) {
throw new Error("ORDER_TABLE_NAME is not defined");
}
if (!ORDER_HOLD_TABLE_NAME) {
throw new Error("ORDER_HOLD_TABLE_NAME is not defined");
export interface MongoOrder {
_id: string;
items: MongoOrderItem[];
transactionId: string;
transactionTime: string | null;
paymentMethod: string;
customerEmail: string;
status: OrderStatus;
}

interface DynamoOrderItem {
export interface MongoOrderItem {
id: string;
image: string;
quantity: number;
name: string;
image?: string;
color: string;
size: string;
price: number;
name: string;
colorway: string;
// product_category: string;
}

interface DynamoOrder {
orderID: string;
paymentGateway: string;
orderItems: DynamoOrderItem[];
status: OrderStatus;
customerEmail: string;
transactionID: string;
orderDateTime: string;
quantity: number;
}

interface DynamoOrderHoldEntry {
// todo
export const getOrders = async (): Promise<OrderDocument[]> => {
const orders = await readTable(OrderModel);
// TODO: decode orders
return orders;
}

export const getOrder = async (id: string) => {
const dynamoOrder = await readItem<DynamoOrder>(
ORDER_TABLE_NAME ?? "",
id,
"orderID"
);
return decodeOrder(dynamoOrder);
export const getOrder = async (id: string): Promise<Order> => {
const order = await readItem<OrderDocument>(OrderModel, id, "_id");
return decodeOrder(order);
};

const decodeOrder = (order: DynamoOrder): Order => {
let date: string | null;
try {
date = new Date(order.orderDateTime).toISOString();
} catch (e) {
date = null;
export const createOrder = async (order: Order): Promise<Order | null> => {
const mongoOrder = encodeOrder(order);
const newItem = await writeItem<OrderDocument>(OrderModel, mongoOrder as OrderDocument);
if (!newItem) {
return null;
}
return decodeOrder(newItem);
};

const decodeOrder = (order: MongoOrder): Order => {
const date = order.transactionTime
? new Date(order.transactionTime).toISOString()
: new Date().toISOString();
return {
id: order.orderID || "",
payment_method: order.paymentGateway || "",
items: order.orderItems.map((item) => ({
id: order._id || "",
paymentMethod: order.paymentMethod || "",
items: order.items.map((item: MongoOrderItem) => ({
id: item.id || "",
name: item.name || "",
// category: item.product_category || "",
image: item.image || undefined,
color: item.colorway || "",
color: item.color || "",
size: item.size || "",
price: item.price || 0,
quantity: item.quantity || 1,
})),
status: order.status || OrderStatus.PENDING_PAYMENT,
customer_email: order.customerEmail || "",
transaction_id: order.transactionID || "",
transaction_time: date || null,
customerEmail: order.customerEmail || "",
transactionId: order.transactionId || "",
transactionTime: date || null,
};
};

const encodeOrderItem = (item: OrderItem): DynamoOrderItem => ({
const encodeOrderItem = (item: OrderItem): OrderItem => ({
id: item.id,
image: item.image ? item.image : "",
quantity: item.quantity,
size: item.size,
price: item.price,
name: item.name,
colorway: item.color,
// product_category: item.category,
color: item.color,
});

const encodeOrder = (order: Order): DynamoOrder => ({
transactionID: order.transaction_id || "",
orderID: order.id,
paymentGateway: order.payment_method || "",
orderItems: order.items.map(encodeOrderItem),
const encodeOrder = (order: Order): MongoOrder => ({
transactionId: order.transactionId || "",
_id: order.id,
paymentMethod: order.paymentMethod || "",
items: order.items.map(encodeOrderItem),
status: order.status || OrderStatus.PENDING_PAYMENT,
customerEmail: order.customer_email || "",
orderDateTime: order.transaction_time
? new Date(order.transaction_time).toISOString()
customerEmail: order.customerEmail || "",
transactionTime: order.transactionTime
? new Date(order.transactionTime).toISOString()
: new Date().toISOString(),
});

export const createOrder = async (order: Order): Promise<Order> => {
const dynamoOrder = encodeOrder(order);
dynamoOrder.orderID = uuidv4();
await writeItem<DynamoOrder>(ORDER_TABLE_NAME, dynamoOrder);
return decodeOrder(dynamoOrder);
};

/*
const encodeOrderHoldEntry = (
_orderHoldEntry: OrderHoldEntry
): DynamoOrderHoldEntry => {
Expand All @@ -117,3 +103,4 @@ export const createOrderHoldEntry = async (
const dynamoOrderHoldEntry = encodeOrderHoldEntry(orderHoldEntry);
await writeItem(ORDER_HOLD_TABLE_NAME, dynamoOrderHoldEntry);
};
*/
12 changes: 10 additions & 2 deletions apps/merch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { index, notFound } from "./routes/index";
import { orderGet } from "./routes/orders";
import { productGet, productsAll } from "./routes/products";
import { appRouter, trpcMiddleware } from "./trpc/router";
import { setupDb } from "./db/mongodb";

const app = express();
const CORS_ORIGIN = process.env.CORS_ORIGIN;
Expand Down Expand Up @@ -49,6 +50,13 @@ app.use("/trpc-panel", (_, res) => {
app.use(notFound);

const port = 3002;
app.listen(port, () => Logger.info(`server started on port ${port}`));

app.listen(port, () => {
(async () => {
await setupDb();
Logger.info(`Server started on port ${port}`);
})().catch((error) => {
const errorMessage = (error as Error).message
Logger.error(`Failed to start server: ${errorMessage}`);
});
});
export default app;
22 changes: 22 additions & 0 deletions apps/merch/src/models/Order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import mongoose, { Schema, Document } from "mongoose";
import { OrderStatus } from "types";
import { OrderItemSchema } from "./OrderItem";
import { MongoOrder } from "../db";

export type OrderDocument = MongoOrder & Document;

const OrderSchema: Schema = new Schema<OrderDocument>({
_id: { type: String, required: true },
items: [ OrderItemSchema ],
transactionId: { type: String, default: "" },
transactionTime: { type: String, default: null },
paymentMethod: { type: String, default: "" },
customerEmail: { type: String, default: "" },
status: {
type: Number,
enum: Object.values(OrderStatus).filter(value => typeof value === 'number'), // Only use numeric values
default: OrderStatus.PENDING_PAYMENT
},
});

export const OrderModel = mongoose.model<OrderDocument>("Order", OrderSchema);
16 changes: 16 additions & 0 deletions apps/merch/src/models/OrderItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import mongoose, { Schema, Document } from "mongoose";
import { MongoOrderItem } from "../db";

export type OrderItemDocument = MongoOrderItem & Document;

export const OrderItemSchema: Schema = new Schema({
id: { type: String, required: true },
name: { type: String, required: true },
image: { type: String, required: false },
color: { type: String, required: true },
size: { type: String, required: true },
price: { type: Number, required: true },
quantity: { type: Number, required: true },
});

export const OrderItemModel = mongoose.model<OrderItemDocument>("OrderItem", OrderItemSchema);
19 changes: 11 additions & 8 deletions apps/merch/src/routes/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const checkout = (req: Request, res: Response<CheckoutResponse>) => {
});
}

const orderID = uuidv4();
const orderId = uuidv4();
const orderTime = new Date();
const expiryTimeMillis = orderTime.getTime() + ORDER_EXPIRY_TIME;
const expiryTime = new Date(expiryTimeMillis);
Expand All @@ -74,14 +74,14 @@ export const checkout = (req: Request, res: Response<CheckoutResponse>) => {
description: `SCSE Merch Purchase:\n${describeCart(
products,
cart,
orderID
orderId
)}`,
}),
])
)
.then(([cart, stripeIntent]) => {
console.log("creating order");
const transactionID = stripeIntent.id;
const transactionId = stripeIntent.id;
const orderItems = cart.items.map(
(item): OrderItem => ({
id: item.id,
Expand All @@ -99,12 +99,12 @@ export const checkout = (req: Request, res: Response<CheckoutResponse>) => {
// })
// );
const order: Order = {
id: orderID,
id: orderId,
items: orderItems,
transaction_id: transactionID,
transaction_time: orderTime.toISOString(),
payment_method: "stripe",
customer_email: email,
transactionId: transactionId,
transactionTime: orderTime.toISOString(),
paymentMethod: "stripe",
customerEmail: email,
status: OrderStatus.PENDING_PAYMENT,
};
// const orderHold: OrderHold = {
Expand All @@ -128,6 +128,9 @@ export const checkout = (req: Request, res: Response<CheckoutResponse>) => {
})
.then(([order, stripeIntent]) => {
console.log("order created");
if (!order) {
return; // something went wrong
}
res.json({
...order,
expiry: expiryTime.toISOString(),
Expand Down
6 changes: 3 additions & 3 deletions apps/merch/src/routes/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const orderGet = (req: Request<"id">, res: Response<Order>) => {
};

const censorDetails = (order: Order): Order => {
const customerEmail = order.customer_email.split("@");
const customerEmail = order.customerEmail.split("@");
return {
...order,
customer_email: starCensor(customerEmail[0]) + "@" + customerEmail.slice(1).join("@"),
transaction_id: starCensor(order.transaction_id),
customerEmail: starCensor(customerEmail[0]) + "@" + customerEmail.slice(1).join("@"),
transactionId: starCensor(order.transactionId),
};
};

Expand Down
6 changes: 3 additions & 3 deletions apps/web/features/merch/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ export class Api {
return res;
}

async getOrder(orderID: string): Promise<Product> {
if (!orderID) {
async getOrder(orderId: string): Promise<Product> {
if (!orderId) {
throw new Error("No order ID");
}
const res = await this.get<Product>(`/orders/${orderID}`);
const res = await this.get<Product>(`/orders/${orderId}`);
return res;
}

Expand Down
Loading
Loading