Skip to content
Merged
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
9 changes: 8 additions & 1 deletion apps/api-gateway/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ app.post('/order/api/create-order', express.raw({ type: 'application/json' }), (
'upgrade',
];

// Set all headers BEFORE piping the response
Object.keys(proxyRes.headers).forEach((key) => {
if (!hopByHopHeaders.includes(key.toLowerCase())) {
const value = proxyRes.headers[key];
Expand All @@ -141,7 +142,13 @@ app.post('/order/api/create-order', express.raw({ type: 'application/json' }), (
}
});

// Forward response body
// Check if headers have already been sent before piping
if (res.headersSent) {
console.error('Headers already sent, cannot pipe response');
return;
}

// Forward response body AFTER all headers are set
proxyRes.pipe(res);
});

Expand Down
69 changes: 60 additions & 9 deletions apps/order-service/src/controller/order.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Stripe from 'stripe';
import redis from '@packages/libs/redis';

import prisma from '@packages/libs/prisma';
import crypto from 'crypto';
import { processOrdersForAllShops } from '../utils/order.utils';
import { generateCartHash, normalizeCart } from '../utils/cart.utils';
import { CartItem, SellerData, PaymentSession, Coupon } from '../types';
import { Request, Response, NextFunction } from 'express';

import { generateCartHash, normalizeCart } from '../utils/cart.utils';
import { CartItem, SellerData, PaymentSession } from '../types';
import { processOrdersForAllShops, resolveCouponsForCart } from '../utils/order.utils';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);

// Request interfaces
Expand All @@ -16,7 +16,7 @@ interface CreatePaymentSessionRequest extends Request {
cart: CartItem[];
userId: string;
selectedAddressId?: string;
coupon?: Coupon;
couponCodes?: string[];
};
}

Expand Down Expand Up @@ -142,14 +142,25 @@ export const verifyPaymentSession = async (req: VerifySessionRequest, res: Respo
});
}

const { allocation } = await resolveCouponsForCart(sessionData.cart, sessionData.couponCodes || []);

// Calculate discounted total
const totalDiscount = allocation.summary.totalDiscount || 0;
const discountedTotal = sessionData.totalAmount - totalDiscount;

return res.status(200).json({
success: true,
session: {
sessionId: sessionData.sessionId,
cart: sessionData.cart,
totalAmount: sessionData.totalAmount,
discountedTotal: discountedTotal,
shippingAddressId: sessionData.shippingAddressId,
coupon: sessionData.coupon,
couponCodes: sessionData.couponCodes || [],
appliedCoupons: allocation.summary,
perItemCoupons: allocation.perItemCoupons,
invalidCouponCodes: allocation.invalidCouponCodes,
unappliedCouponCodes: allocation.unappliedCouponCodes,
sellers: sessionData.sellers,
},
});
Expand All @@ -163,6 +174,36 @@ export const verifyPaymentSession = async (req: VerifySessionRequest, res: Respo
}
};

export const previewCartCoupons = async (req: Request, res: Response) => {
try {
const { cart, couponCodes = [] } = req.body as { cart: CartItem[]; couponCodes?: string[] };

if (!Array.isArray(cart) || cart.length === 0) {
return res.status(400).json({
success: false,
message: 'Cart must be a non-empty array',
});
}

const { allocation, missingCouponCodes } = await resolveCouponsForCart(cart, couponCodes || []);

return res.status(200).json({
success: true,
appliedCoupons: allocation.summary,
perItemCoupons: allocation.perItemCoupons,
invalidCouponCodes: allocation.invalidCouponCodes.concat(missingCouponCodes),
unappliedCouponCodes: allocation.unappliedCouponCodes,
});
} catch (error) {
console.error('Error previewing coupons:', error);
return res.status(500).json({
success: false,
message: 'Failed to preview coupons',
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};

export const createPaymentIntent = async (req: CreatePaymentIntentRequest, res: Response) => {
try {
const { amount, stripeSellerAccountId, sessionId } = req.body;
Expand Down Expand Up @@ -234,7 +275,7 @@ export const createPaymentIntent = async (req: CreatePaymentIntentRequest, res:
*/
export const createPaymentSession = async (req: CreatePaymentSessionRequest, res: Response) => {
try {
const { cart, userId, selectedAddressId, coupon } = req.body;
const { cart, userId, selectedAddressId, couponCodes = [] } = req.body;

// Validate required fields
if (!cart || !userId) {
Expand Down Expand Up @@ -320,14 +361,20 @@ export const createPaymentSession = async (req: CreatePaymentSessionRequest, res

// Create session payload using normalized cart\
//TODO: should we use car
const { allocation, sanitizedCouponCodes, missingCouponCodes } = await resolveCouponsForCart(cart, couponCodes);

const sessionData: PaymentSession = {
userId,
sessionId,
cart: cart,
cart,
sellers: sellerData,
totalAmount: cartTotal,
shippingAddressId: selectedAddressId || undefined,
coupon: coupon || undefined,
couponCodes: sanitizedCouponCodes,
appliedCoupons: allocation.summary,
perItemCoupons: allocation.perItemCoupons,
invalidCouponCodes: allocation.invalidCouponCodes.concat(missingCouponCodes),
unappliedCouponCodes: allocation.unappliedCouponCodes,
};

// Store session with simple key structure and 10-minute expiration
Expand All @@ -341,6 +388,10 @@ export const createPaymentSession = async (req: CreatePaymentSessionRequest, res
success: true,
sessionId,
isExisting: false,
appliedCoupons: allocation.summary,
perItemCoupons: allocation.perItemCoupons,
invalidCouponCodes: allocation.invalidCouponCodes.concat(missingCouponCodes),
unappliedCouponCodes: allocation.unappliedCouponCodes,
});
} catch (error) {
console.error('Error creating payment session:', error);
Expand Down
2 changes: 2 additions & 0 deletions apps/order-service/src/routes/order.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createPaymentIntent,
createPaymentSession,
verifyPaymentSession,
previewCartCoupons,
getSellerOrders,
getOrderDetails,
updateOrderStatus,
Expand All @@ -26,6 +27,7 @@ router.get('/health', (req, res) => {
// Payment routes with validation
router.post('/create-payment-intent', validateCreatePaymentIntent, isAuthenticated, createPaymentIntent);
router.post('/create-payment-session', validateCreatePaymentSession, createPaymentSession);
router.post('/preview-coupons', previewCartCoupons);

// Session verification route
router.get('/verify-session/:sessionId', verifyPaymentSession);
Expand Down
47 changes: 41 additions & 6 deletions apps/order-service/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,39 @@ export interface CartItem {
selectedOptions?: Record<string, any>;
}

export type CouponDiscountType = 'PERCENT' | 'FLAT';

export interface Coupon {
code: string;
discountPercent?: number;
discountAmount?: number;
discountedProductId?: string;
discountType: CouponDiscountType;
discountValue: number;
}

export interface AppliedCoupon {
code: string;
discountAmount: number;
appliedProductIds: string[];
discountType: CouponDiscountType;
discountValue: number;
}

export interface AppliedCouponSummary {
totalDiscount: number;
coupons: AppliedCoupon[];
}

export interface OrderItemCoupon {
code: string;
discountAmount: number;
discountType: CouponDiscountType;
discountValue: number;
}

export interface CouponAllocationResult {
perItemCoupons: Record<string, OrderItemCoupon | undefined>;
summary: AppliedCouponSummary;
invalidCouponCodes: string[];
unappliedCouponCodes: string[];
}

export interface SellerData {
Expand All @@ -27,14 +55,21 @@ export interface PaymentSession {
sellers: SellerData[];
totalAmount: number;
shippingAddressId?: string;
coupon?: Coupon;
couponCodes?: string[];
perItemCoupons?: Record<string, OrderItemCoupon | undefined>;
appliedCoupons?: AppliedCouponSummary;
invalidCouponCodes?: string[];
unappliedCouponCodes?: string[];
subtotal?: number;
}

export interface OrderItemData {
productId: string;
quantity: number;
price: number;
selectedOptions?: Record<string, any>;
discountAmount?: number;
coupon?: OrderItemCoupon | null;
}

export interface OrderData {
Expand All @@ -43,8 +78,8 @@ export interface OrderData {
total: number;
status: 'PENDING' | 'PAID' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
shippingAddressId?: string;
couponCode?: string;
discountAmount: number;
totalDiscount: number;
appliedCoupons?: AppliedCouponSummary;
items: OrderItemData[];
}

Expand Down
15 changes: 11 additions & 4 deletions apps/order-service/src/utils/cart.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ export const normalizeCart = (cart: any[]) => {
const uniqueItems = new Map();

cart.forEach((item) => {
const key = `${item.productId}_${item.variantId || 'default'}`;
if (!item || !item.id) {
return;
}

const key = `${item.id}_${item.variantId || 'default'}`;
if (uniqueItems.has(key)) {
// If duplicate found, sum the quantities
uniqueItems.get(key).quantity += item.quantity;
const existing = uniqueItems.get(key);
existing.quantity += item.quantity;
existing.sale_price = item.sale_price;
existing.shopId = item.shopId;
existing.selectedOptions = item.selectedOptions;
} else {
uniqueItems.set(key, { ...item });
}
});

// Convert back to array and sort by product ID
// The JSON.stringify in generateCartHash will handle the normalization
return Array.from(uniqueItems.values()).sort((a, b) => a.productId.localeCompare(b.productId));
return Array.from(uniqueItems.values()).sort((a, b) => a.id.localeCompare(b.id));
};

import crypto from 'crypto';
Expand Down
Loading
Loading