Skip to content

Commit 802db97

Browse files
committed
feat: changing sell process sales service method
1 parent 268195e commit 802db97

File tree

4 files changed

+121
-115
lines changed

4 files changed

+121
-115
lines changed

src/controllers/SellController.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ export default class SellController extends BaseController {
3333
* - Calls `SellService.processSale` to create the customer, bill, and sales.
3434
* - Returns the newly created bill with associated sales.
3535
*/
36-
processSale = async (
36+
processSales = async (
3737
req: Request,
3838
res: Response,
3939
next: NextFunction,
4040
): Promise<void> => {
4141
try {
42-
const { customer, sales } = req.body;
42+
const { sales: saleDTO } = req.body;
4343

44-
logger.info(`🛒 [SellController] Processing sale for: ${customer.email}`);
44+
logger.info(`🛒 [SellController] Processing sale`);
4545

46-
const bill = await this.sellServiceImpl?.processSale(customer, sales);
46+
const isSaleProcessed = await this.sellServiceImpl?.processSales(saleDTO);
4747

4848
res
4949
.status(httpStatus.CREATED.code)
@@ -52,13 +52,9 @@ export default class SellController extends BaseController {
5252
httpStatus.CREATED.code,
5353
httpStatus.CREATED.status,
5454
'Sale processed successfully',
55-
bill,
55+
isSaleProcessed,
5656
),
5757
);
58-
59-
logger.info(
60-
`✅ [SellController] Sale processed under bill ID: ${bill?.id}`,
61-
);
6258
} catch (error) {
6359
logger.error(`❌ [SellController] Sale processing failed`, { error });
6460
next(error);

src/routes/sell.router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ router.post(
7878
verifyToken,
7979
sellValidation,
8080
validateRequest,
81-
sellController.processSale,
81+
sellController.processSales,
8282
);
8383

8484
/**

src/services/SellService.ts

Lines changed: 105 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import { Customer } from '../models/Customer';
44
import { SellRepository } from '../repositories/SellRepository';
55
import { BillRepository } from '../repositories/BillRepository';
66
import { CustomerRepository } from '../repositories/CustomerRepository';
7-
import { ProductService } from '../services/ProductService';
87
import { GenericService } from './GenericService';
98
import { Cache } from '../utils/cacheUtil';
109
import logger from '../utils/logger';
1110
import { inject, injectable } from 'tsyringe';
1211
import { BaseAppException } from '../errors/BaseAppException';
13-
import { uploadFile } from '../utils/s3Util'; // S3 utility
14-
import { sendEmail } from '../utils/sesUtil'; // SES utility
15-
import { customer_receipt } from '../email_templates/templates'; // hypothetical utility to format bill
12+
import { uploadFile } from '../utils/s3Util';
13+
import { sendEmail } from '../utils/sesUtil';
14+
import { customer_receipt } from '../email_templates/templates';
15+
import { ProductRepository } from '../repositories/ProductRepository';
16+
17+
export interface ProcessSalesDTO {
18+
customerId: number;
19+
sales: Array<{
20+
productId: number;
21+
quantity: number;
22+
sale_price: number;
23+
}>;
24+
}
1625

1726
@injectable()
1827
export class SellService extends GenericService<Sale> {
@@ -22,98 +31,129 @@ export class SellService extends GenericService<Sale> {
2231
@inject('BillRepository') private readonly billRepository: BillRepository,
2332
@inject('CustomerRepository')
2433
private readonly customerRepository: CustomerRepository,
25-
@inject('ProductService') private readonly productService: ProductService,
34+
@inject('ProductRepository')
35+
private readonly productRepository: ProductRepository,
2636
) {
2737
super(cache, sellRepository, Sale);
2838
logger.info('✅ [SellService] Initialized SellService');
2939
this.sellRepository = sellRepository;
3040
this.billRepository = billRepository;
3141
this.customerRepository = customerRepository;
32-
this.productService = productService;
42+
this.productRepository = productRepository;
3343
}
3444

35-
async processSale(
36-
customerData: Omit<Customer, 'id' | 'bills'>,
37-
sales: { productId: number; quantity: number; salePrice: number }[],
38-
): Promise<Bill> {
45+
async processSales(salesDTO: ProcessSalesDTO): Promise<boolean> {
46+
logger.info('🚀 Starting processSales...');
47+
48+
// 1️⃣ Retrieve the customer
49+
const customer: Customer | null =
50+
await this.customerRepository.findEntityById(salesDTO.customerId);
51+
if (!customer) {
52+
logger.error('❌ Customer not found with id: %s', salesDTO.customerId);
53+
throw new BaseAppException('Customer not found');
54+
}
3955
logger.info(
40-
`🛒 [SellService] Processing sale for new customer: ${customerData.email}`,
56+
'👤 Found customer: %s %s',
57+
customer.first_name,
58+
customer.last_name,
4159
);
4260

43-
const newCustomer = new Customer();
44-
Object.assign(newCustomer, customerData);
45-
const savedCustomer =
46-
await this.customerRepository.createEntity(newCustomer);
47-
48-
const newBill = new Bill();
49-
newBill.customer = savedCustomer!;
50-
newBill.total_amount = 0;
51-
newBill.sales = [];
52-
53-
const savedBill = await this.billRepository.createEntity(newBill);
54-
55-
let totalAmount = 0;
56-
57-
for (const sale of sales) {
58-
const product = await this.productService.findById(sale.productId);
59-
if (!product || product.available_quantity < sale.quantity) {
61+
// 2️⃣ Create a new Bill for the customer
62+
const bill = new Bill();
63+
bill.customer = customer;
64+
bill.total_amount = 0;
65+
bill.sales = [];
66+
logger.info('🧾 New bill created for customer');
67+
68+
// 3️⃣ Process each sale item
69+
for (const saleDTO of salesDTO.sales) {
70+
logger.info(
71+
'🔍 Processing sale item for product id: %s',
72+
saleDTO.productId,
73+
);
74+
75+
const product = await this.productRepository.findEntityById(
76+
saleDTO.productId,
77+
);
78+
if (!product) {
79+
logger.error('❌ Product not found with id: %s', saleDTO.productId);
6080
throw new BaseAppException(
61-
`❌ Insufficient stock or invalid product ID: ${sale.productId}`,
81+
`Product with id ${saleDTO.productId} not found`,
6282
);
6383
}
84+
logger.info('📦 Found product: %s', product.name);
6485

65-
const newSale = new Sale();
66-
newSale.bill = savedBill!;
67-
newSale.product = product;
68-
newSale.quantity = sale.quantity;
69-
newSale.sale_price = sale.salePrice;
86+
if (product.available_quantity < saleDTO.quantity) {
87+
logger.error('❌ Insufficient stock for product: %s', product.name);
88+
throw new BaseAppException(
89+
`Insufficient stock for product ${product.name}`,
90+
);
91+
}
7092

71-
await this.sellRepository.createEntity(newSale);
72-
newBill.sales.push(newSale);
93+
// Update product stock
94+
product.available_quantity -= saleDTO.quantity;
95+
await this.productRepository.updateEntity(saleDTO.productId, product);
96+
logger.info(
97+
'✅ Updated stock for product %s, new quantity: %s',
98+
product.name,
99+
product.available_quantity,
100+
);
101+
102+
// Create and add Sale
103+
const sale = new Sale();
104+
sale.bill = bill;
105+
sale.product = product;
106+
sale.quantity = saleDTO.quantity;
107+
sale.sale_price = saleDTO.sale_price;
108+
bill.sales.push(sale);
109+
bill.total_amount += sale.sale_price * sale.quantity;
110+
logger.info(
111+
'🛍️ Added sale item: %s units of %s at $%s each',
112+
sale.quantity,
113+
product.name,
114+
sale.sale_price,
115+
);
116+
}
73117

74-
totalAmount += sale.quantity * sale.salePrice;
118+
// 4️⃣ Save the Bill (cascading the sales)
119+
const savedBill = await this.billRepository.createEntity(bill);
75120

76-
await this.productService.update(product.id, {
77-
available_quantity: product.available_quantity - sale.quantity,
78-
});
121+
if (!savedBill) {
122+
logger.error('❌ Error saving bill');
123+
throw new BaseAppException('Error saving bill');
79124
}
80125

81-
savedBill!.total_amount = totalAmount;
82-
const finalBill = await this.billRepository.updateEntity(savedBill!.id, {
83-
total_amount: totalAmount,
84-
});
126+
logger.info(
127+
'💾 Bill saved with id: %s, Total amount: $%s',
128+
savedBill.id,
129+
savedBill.total_amount,
130+
);
85131

86-
// 🧾 Upload the bill HTML to S3
87-
const billHtml = customer_receipt(finalBill!);
88-
const fileName = `bill-${finalBill!.id}.html`;
132+
// 5️⃣ Generate a receipt and upload it to S3
133+
const billHtml = customer_receipt(savedBill);
134+
const fileName = `${customer.first_name}-${customer.last_name}-bill-${savedBill.date.toISOString()}.html`;
89135
const contentType = 'text/html';
90136
const bucket = process.env.S3_BUCKET_BILL ?? 'default-bill-bucket';
91-
const fileUrl = await uploadFile(
92-
fileName,
93-
Buffer.from(billHtml),
94-
contentType,
95-
bucket,
96-
);
137+
await uploadFile(fileName, Buffer.from(billHtml), contentType, bucket);
138+
logger.info('☁️ Receipt uploaded to S3: %s', fileName);
97139

98-
logger.info(`📤 [SellService] Bill uploaded to S3: ${fileUrl}`);
99-
100-
// 📩 Email the bill
140+
// 6️⃣ Send the receipt via email to the customer
101141
await sendEmail(
102-
[savedCustomer!.email],
103-
`Your Purchase Receipt - Bill #${finalBill!.id}`,
142+
[customer.email],
143+
`Your Purchase Receipt - Bill #${savedBill.id}`,
104144
billHtml,
105145
);
146+
logger.info('📧 Receipt email sent to: %s', customer.email);
106147

107-
logger.info(
108-
`📧 [SellService] Bill sent to customer: ${savedCustomer!.email}`,
109-
);
110-
148+
// 7️⃣ Cache the saved bill
111149
await this.cache.set(
112-
`bill:${finalBill!.id}`,
113-
JSON.stringify(finalBill),
150+
`bill:${savedBill.id}`,
151+
JSON.stringify(savedBill),
114152
3600,
115153
);
154+
logger.info('🔒 Bill cached with key: bill:%s', savedBill.id);
116155

117-
return finalBill!;
156+
logger.info('🎉 processSales completed successfully.');
157+
return true;
118158
}
119159
}

src/utils/inputValidation.ts

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -143,61 +143,31 @@ export const productValidation = [
143143
* Ensures valid input fields for processing a sale.
144144
*/
145145
export const sellValidation = [
146-
body('customer.email')
146+
body('customerId')
147147
.notEmpty()
148148
.withMessage(validationMessage.REQUIRED)
149-
.isEmail()
150-
.withMessage(validationMessage.INVALID_EMAIL),
151-
152-
body('customer.first_name')
153-
.notEmpty()
154-
.withMessage(validationMessage.REQUIRED)
155-
.isString()
156-
.withMessage('📝 First name must be a string')
157-
.isLength({ min: 2, max: 50 })
158-
.withMessage(validationMessage.STRING_LENGTH(2, 50)),
159-
160-
body('customer.last_name')
161-
.notEmpty()
162-
.withMessage(validationMessage.REQUIRED)
163-
.isString()
164-
.withMessage('📝 Last name must be a string')
165-
.isLength({ min: 2, max: 50 })
166-
.withMessage(validationMessage.STRING_LENGTH(2, 50)),
167-
168-
body('customer.address')
169-
.optional()
170-
.isString()
171-
.withMessage('📍 Address must be a string')
172-
.isLength({ min: 5, max: 255 })
173-
.withMessage(validationMessage.STRING_LENGTH(5, 255)),
174-
175-
body('customer.phonenumber')
176-
.optional()
177-
.isString()
178-
.withMessage('📞 Phone number must be a string')
179-
.isLength({ min: 8, max: 20 })
180-
.withMessage(validationMessage.STRING_LENGTH(8, 20)),
149+
.isNumeric()
150+
.withMessage('Customer ID must be a number'),
181151

182152
body('sales')
183153
.isArray({ min: 1 })
184-
.withMessage('🛍️ Sales must be an array with at least one item'),
154+
.withMessage('Sales must be an array with at least one sale item'),
185155

186156
body('sales.*.productId')
187157
.notEmpty()
188158
.withMessage(validationMessage.REQUIRED)
189159
.isNumeric()
190-
.withMessage(validationMessage.INVALID_NUMBER),
160+
.withMessage('Product ID must be a number'),
191161

192162
body('sales.*.quantity')
193163
.notEmpty()
194164
.withMessage(validationMessage.REQUIRED)
195-
.isInt({ min: 1 })
196-
.withMessage(validationMessage.POSITIVE_NUMBER),
165+
.isInt({ gt: 0 })
166+
.withMessage('Quantity must be a positive integer'),
197167

198-
body('sales.*.salePrice')
168+
body('sales.*.sale_price')
199169
.notEmpty()
200170
.withMessage(validationMessage.REQUIRED)
201-
.isFloat({ min: 0.01 })
202-
.withMessage(validationMessage.POSITIVE_NUMBER),
171+
.isFloat({ gt: 0 })
172+
.withMessage('Sale price must be a positive number'),
203173
];

0 commit comments

Comments
 (0)