"feat: enhance order management with new APIs and schema changes"

This commit is contained in:
louiscklaw
2025-05-30 11:40:25 +08:00
parent 834f58bde1
commit 5a707427c6
32 changed files with 1004 additions and 122 deletions

View File

@@ -27,7 +27,7 @@
"unseed": "tsx ./prisma/unseed.ts",
"db:generate": "prisma generate",
"db:push": "prisma db push --force-reset",
"db:push:w": "npx nodemon --delay 5 --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed && yarn db:studio\"",
"db:push:w": "npx nodemon --delay 5 --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed\"",
"db:studio": "prisma studio"
},
"engines": {

View File

@@ -266,85 +266,85 @@ model Mail {
// attachments MailAttachment[]
}
model OrderHistory {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
orderTime DateTime
paymentTime DateTime
deliveryTime DateTime
completionTime DateTime
timeline Json[]
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderHistory {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// orderTime DateTime @default(now())
// paymentTime DateTime
// deliveryTime DateTime
// completionTime DateTime
// timeline Json[]
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderShippingAddress {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
fullAddress String
phoneNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderShippingAddress {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// fullAddress String
// phoneNumber String
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderPayment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
cardType String
cardNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderPayment {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// cardType String
// cardNumber String
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderDelivery {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
shipBy String
speedy String
trackingNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderDelivery {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// shipBy String
// speedy String
// trackingNumber String
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderCustomer {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
name String
email String
avatarUrl String
ipAddress String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderCustomer {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// name String
// email String
// avatarUrl String
// ipAddress String
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderProductItem {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
sku String
name String
price Float
coverUrl String
quantity Float
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int?
}
// model OrderProductItem {
// id Int @id @default(autoincrement())
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// //
// sku String
// name String
// price Float
// coverUrl String
// quantity Float
// OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
// orderItemId Int?
// }
model OrderItem {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
taxes Float
status String
@@ -354,12 +354,18 @@ model OrderItem {
orderNumber String
totalAmount Float
totalQuantity Float
history OrderHistory[]
payment OrderPayment[]
customer OrderCustomer[]
delivery OrderDelivery[]
items OrderProductItem[]
shippingAddress OrderShippingAddress[]
history Json
payment Json
customer Json
delivery Json
items Json[]
shippingAddress Json
// OrderProductItem OrderProductItem[]
// OrderHistory OrderHistory[]
// OrderDelivery OrderDelivery[]
// OrderCustomer OrderCustomer[]
// OrderPayment OrderPayment[]
// OrderShippingAddress OrderShippingAddress[]
}
// src/types/tour.ts

View File

@@ -23,6 +23,8 @@ import { ProductReview } from './seeds/productReview';
import { ProductItem } from './seeds/productItem';
import { FileStore } from './seeds/fileStore';
import { userItemSeed } from './seeds/userItem';
import { orderItemSeed } from './seeds/orderItem';
//
// import { Blog } from './seeds/blog';
// import { Mail } from './seeds/mail';
@@ -39,6 +41,7 @@ import { userItemSeed } from './seeds/userItem';
await FileStore;
await ProductItem;
await userItemSeed;
await orderItemSeed;
// await Blog;
// await Mail;
// await File;

View File

@@ -10,8 +10,8 @@ async function order() {
title: 'Single Party with Dating',
order_time: new Date(),
last_payment_date: new Date(),
status: 'Pending'
}
status: 'Pending',
},
});
}

View File

@@ -0,0 +1,94 @@
import { PrismaClient } from '@prisma/client';
import { _mock } from './_mock';
const prisma = new PrismaClient();
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
id: _mock.id(index),
sku: `16H9UR${index}`,
quantity: index + 1,
name: _mock.productName(index),
coverUrl: _mock.image.product(index),
price: _mock.number.price(index),
}));
async function orderItem() {
await prisma.orderItem.deleteMany({});
for (let index = 1; index < 20 + 1; index++) {
const shipping = 10;
const discount = 10;
const taxes = 10;
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
const totalAmount = subtotal - shipping - discount + taxes;
const customer = {
id: _mock.id(index),
name: _mock.fullName(index),
email: _mock.email(index),
avatarUrl: _mock.image.avatar(index),
ipAddress: '192.158.1.38',
};
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
const history = {
orderTime: _mock.time(1),
paymentTime: _mock.time(2),
deliveryTime: _mock.time(3),
completionTime: _mock.time(4),
timeline: [
{ title: 'Delivery successful', time: _mock.time(1) },
{ title: 'Transporting to [2]', time: _mock.time(2) },
{ title: 'Transporting to [1]', time: _mock.time(3) },
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
{ title: 'Order has been created', time: _mock.time(5) },
],
};
const temp = await prisma.orderItem.upsert({
where: { id: index.toString() },
update: {},
create: {
id: index.toString(),
orderNumber: `#601${index}`,
taxes,
items,
history,
subtotal: items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0),
shipping,
discount,
customer,
delivery,
totalAmount,
totalQuantity,
shippingAddress: {
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
phoneNumber: '365-374-4961',
},
payment: {
//
cardType: 'mastercard',
cardNumber: '4111 1111 1111 1111',
},
status: (index % 2 && 'completed') || (index % 3 && 'pending') || (index % 4 && 'cancelled') || 'refunded',
},
});
}
console.log('seed orderItem done');
}
const orderItemSeed = orderItem()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { orderItemSeed };

View File

@@ -0,0 +1,98 @@
// src/app/api/product/saveProduct/route.ts
//
// PURPOSE:
// save product to db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* PUT - change order status
*************************************** */
export async function PUT(req: NextRequest) {
// logger('[Order] list', products.length);
const { searchParams } = req.nextUrl;
const orderId = searchParams.get('orderId');
// RULES: orderId must exist
if (!orderId) {
return response({ message: 'Order ID is required!' }, STATUS.BAD_REQUEST);
}
const { data } = await req.json();
try {
const order = await prisma.orderItem.updateMany({
where: { id: orderId },
data: { status: data.status },
});
return response({ order }, STATUS.OK);
} catch (error) {
console.log({ data });
return handleError('Order - Get list', error);
}
}
export type IProductItem = {
id: string;
sku: string;
name: string;
code: string;
price: number;
taxes: number;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: string[];
colors: string[];
quantity: number;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
// createdAt: IDateValue;
inventoryType: string;
subDescription: string;
priceSale: number | null;
// reviews: IProductReview[];
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
ratings: {
name: string;
starCount: number;
reviewCount: number;
}[];
};
export type IDateValue = string | number | null;
export type IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,9 @@
###
PUT http://localhost:7272/api/order/changeStatus?orderId=1
content-type: application/json
{
"data":{"status": "helloworld"}
}

View File

@@ -0,0 +1,53 @@
// src/app/api/user/createUser/route.ts
//
// PURPOSE:
// create user to db
//
// RULES:
// T.B.A.
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create User
***************************************
*/
export async function POST(req: NextRequest) {
// logger('[User] list', users.length);
const { data } = await req.json();
const createForm: CreateUserData = data as unknown as CreateUserData;
try {
const user = await prisma.userItem.create({ data: createForm });
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Create', error);
}
}
type CreateUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};

View File

@@ -0,0 +1,4 @@
###
POST http://localhost:7272/api/user/createUser

View File

@@ -0,0 +1,47 @@
// src/app/api/product/deleteUser/route.ts
//
// PURPOSE:
// delete product from db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* handle Delete Users
*************************************** */
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const userId = searchParams.get('userId');
if (!userId) {
return response({ message: 'User ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: userId confirmed exist, run below
const user = await prisma.userItem.delete({
//
where: { id: userId },
});
if (!user) {
return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
}
logger('[User] details', user.id);
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
DELETE http://localhost:7272/api/user/deleteUser?userId=3f431e6f-ad05-4d60-9c25-6a7e92a954ad

View File

@@ -0,0 +1,47 @@
// src/app/api/order/details/route.ts
//
// PURPOSE:
// read order from db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET Order detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: orderId must exist
const orderId = searchParams.get('orderId');
if (!orderId) {
return response({ message: 'orderId is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: orderId confirmed exist, run below
const order = await prisma.orderItem.findFirst({
// include: { reviews: true },
where: { id: orderId.toString() },
});
if (!order) {
return response({ message: 'Order not found!' }, STATUS.NOT_FOUND);
}
logger('[Order] details', order.id);
return response({ order }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/order/details?orderId=1

View File

@@ -0,0 +1,30 @@
// src/app/api/product/image/upload/route.ts
//
// PURPOSE:
// handle upload product image
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
// import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
*************************************** */
export async function POST(req: NextRequest) {
try {
const { data } = await req.json();
console.log('helloworld');
return response({ hello: 'world' }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world' });
return handleError('Product - store product image', error);
}
}

View File

@@ -0,0 +1,22 @@
// src/app/api/order/list/route.ts
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - OrderItem
*************************************** */
export async function GET() {
try {
const orders = await prisma.orderItem.findMany();
logger('[Order] list', orders.length);
return response({ orders }, STATUS.OK);
} catch (error) {
return handleError('OrderItem - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/order/list

View File

@@ -0,0 +1,115 @@
// src/app/api/product/saveProduct/route.ts
//
// PURPOSE:
// save product to db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
*************************************** */
export async function POST(req: NextRequest) {
// logger('[Product] list', products.length);
const { searchParams } = req.nextUrl;
const userId = searchParams.get('userId');
// RULES: userId must exist
if (!userId) {
return response({ message: 'Product ID is required!' }, STATUS.BAD_REQUEST);
}
const { data } = await req.json();
try {
const user = await prisma.userItem.updateMany({
where: { id: userId },
data: {
status: data.status,
avatarUrl: data.avatarUrl,
isVerified: data.isVerified,
name: data.name,
email: data.email,
phoneNumber: data.phoneNumber,
country: data.country,
state: data.state,
city: data.city,
address: data.address,
zipCode: data.zipCode,
company: data.company,
role: data.role,
//
username: data.username,
password: data.password,
},
});
return response({ user }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world', data });
return handleError('Product - Get list', error);
}
}
export type IProductItem = {
id: string;
sku: string;
name: string;
code: string;
price: number;
taxes: number;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: string[];
colors: string[];
quantity: number;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
// createdAt: IDateValue;
inventoryType: string;
subDescription: string;
priceSale: number | null;
// reviews: IProductReview[];
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
ratings: {
name: string;
starCount: number;
reviewCount: number;
}[];
};
export type IDateValue = string | number | null;
export type IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,3 @@
###
POST http://localhost:7272/api/user/list

View File

@@ -0,0 +1,37 @@
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { _products } from 'src/_mock/_product';
// ----------------------------------------------------------------------
export const runtime = 'edge';
/** **************************************
* GET - Search products
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const query = searchParams.get('query')?.trim().toLowerCase();
if (!query) {
return response({ results: [] }, STATUS.OK);
}
const products = _products();
// Accept search by name or sku
const results = products.filter(
({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query)
);
logger('[Product] search-results', results.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('Product - Get search', error);
}
}