Skip to content
Open
8 changes: 4 additions & 4 deletions src/controllers/sellerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ export const addOrUpdateSellerItem = async (req: Request, res: Response) => {

logger.debug('Form data being sent:', { formData });
// Add or update Item
const sellerItem = await sellerService.addOrUpdateSellerItem(currentSeller, formData);
const item = await sellerService.addOrUpdateSellerItem(currentSeller, formData);
logger.info(`Added/ updated seller item for seller ${currentSeller.seller_id}`);

// Send response
return res.status(200).json({ sellerItem: sellerItem });
} catch (error) {
return res.status(200).json(item);
} catch (error: any) {
logger.error(`Failed to add or update seller item for userID ${currentSeller.seller_id}:`, error);
return res.status(500).json({
message: 'An error occurred while adding/ updating seller item; please try again later',
message: error.message || 'An error occurred while adding or updating seller item; please try again later',
});
}
};
Expand Down
6 changes: 6 additions & 0 deletions src/errors/MappiDeductionError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class MappiDeductionError extends Error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

constructor(public pi_uid: string, public amount: number, message: string) {
super(message);
this.name = "MappiDeductionError";
}
}
72 changes: 72 additions & 0 deletions src/helpers/sellerItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import logger from "../config/loggingConfig";
import { ISellerItem } from "../types";

export const isExpiredItem = (item: ISellerItem): boolean => {
if (!item || !item.expired_by) return true;
return new Date() > new Date(item.expired_by);
};

export const getRemainingWeeks = (existing_item: ISellerItem): number => {
if (!existing_item || !existing_item.expired_by || !existing_item.duration) return 0;

const now = new Date();
const expiry = new Date(existing_item.expired_by);

// Calculate total weeks from duration
const totalWeeks = Math.floor(Number(existing_item.duration));

// Calculate weeks left (excluding current week)
const msPerWeek = 7 * 24 * 60 * 60 * 1000;
const msLeft = expiry.getTime() - now.getTime();
const weeksLeft = Math.floor(msLeft / msPerWeek);

// Exclude current week
const remainingWeeks = Math.max(weeksLeft - 1, 0);

// Ensure not more than total duration
return Math.min(remainingWeeks, totalWeeks);
};

export const getChangeInWeeks = (existingItem: ISellerItem, itemData: ISellerItem): number => {
const newDuration = Math.max(Number(itemData.duration) || 1, 1);
const existingDuration = Math.max(Number(existingItem.duration) || 1, 1);

const change = newDuration - existingDuration;

if (change < 0) {
const remainingWeeks = getRemainingWeeks(existingItem);
if (Math.abs(change) > remainingWeeks) {
logger.warn(`Attempted to reduce duration by ${ Math.abs(change) } weeks, but only ${ remainingWeeks } weeks remain.`);
return 0; // Prevent reducing more than remaining weeks
}
}

return change;
};

export const computeNewExpiryDate = (existingItem: ISellerItem, itemData: ISellerItem): Date => {
const now = new Date();
const msPerWeek = 7 * 24 * 60 * 60 * 1000;

if (isExpiredItem(existingItem)) {
// Reset to new duration from now
const newDuration = Math.max(Number(itemData.duration) || 1, 1);
return new Date(now.getTime() + newDuration * msPerWeek);
}

const expiry = new Date(existingItem.expired_by);
const changeInWeeks = getChangeInWeeks(existingItem, itemData);

// If duration increased, extend expiry
if (changeInWeeks > 0) {
return new Date(expiry.getTime() + changeInWeeks * msPerWeek);
}

// If duration decreased, reduce expiry
if (changeInWeeks < 0) {
return new Date(expiry.getTime() + changeInWeeks * msPerWeek);
}

// No change;
return expiry;
};
36 changes: 36 additions & 0 deletions src/services/membership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { IMembership, IUser, MembershipOption } from "../types";

import logger from "../config/loggingConfig";
import { MappiDeductionError } from "../errors/MappiDeductionError";

/* Helper functions */
const handleSingleMappiPurchase = async (
Expand Down Expand Up @@ -278,4 +279,39 @@ export const applyMembershipChange = async (
logger.error(`Failed to apply membership change for ${ piUid }: ${ error }`);
throw error;
}
};

export const deductMappiBalance = async (pi_uid: string, amount: number): Promise<number> => {
try {
if (amount === 0) return 0;

const membership = await Membership.findOne({ pi_uid });
if (!membership) {
throw new MappiDeductionError(pi_uid, amount, 'Membership not found');
}
if ((membership.mappi_balance ?? 0) < amount) {
throw new MappiDeductionError(pi_uid, amount, 'Insufficient Mappi balance');
}

const updatedMembership = await Membership.findOneAndUpdate(
{ pi_uid },
{ $inc: { mappi_balance: -amount } },
{ new: true }
).exec();

if (!updatedMembership) {
throw new MappiDeductionError(pi_uid, amount, 'Failed to deduct Mappi balance');
}

logger.info("consumed Mappi: ", { amount });
return amount;
} catch (error: any) {
if (error instanceof MappiDeductionError) {
logger.error(`MappiDeductionError for piUID ${error.pi_uid}: ${error.message}`);
throw error;
} else {
logger.error(`Unexpected error during Mappi deduction for piUID ${pi_uid}: ${error.message || error}`);
throw new MappiDeductionError(pi_uid, amount, error.message || 'Unknown error');
}
}
};
9 changes: 8 additions & 1 deletion src/services/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { OrderStatusType } from "../models/enums/orderStatusType";
import { OrderItemStatusType } from "../models/enums/orderItemStatusType";
import { IOrder, NewOrder } from "../types";
import logger from "../config/loggingConfig";
import { deductMappiBalance } from "./membership.service";
import { MappiDeductionError } from "../errors/MappiDeductionError";

export const createOrder = async (
orderData: NewOrder,
Expand Down Expand Up @@ -91,7 +93,10 @@ export const createOrder = async (
await SellerItem.bulkWrite(bulkSellerItemUpdates, { session });
}

/* Step 5: Commit the transaction */
/* Step 5: Deduct single mappi for order checkout*/
await deductMappiBalance(orderData.buyerPiUid, 1);

/* Step 6: Commit the transaction */
await session.commitTransaction();
logger.info('Order and stock levels created/updated successfully', { orderId: newOrder._id });

Expand All @@ -101,6 +106,8 @@ export const createOrder = async (

if (error instanceof StockValidationError) {
logger.warn(`Stock validation failed: ${error.message}`, { itemId: error.itemId });
} else if (error instanceof MappiDeductionError) {
logger.error(`MappiDeduction Error for piUID ${error.pi_uid}: ${error.message}`);
} else {
logger.error(`Failed to create order and update stock: ${error}`);
}
Expand Down
101 changes: 70 additions & 31 deletions src/services/seller.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { getUserSettingsById } from "./userSettings.service";
import { IUser, IUserSettings, ISeller, ISellerWithSettings, ISellerItem } from "../types";

import logger from "../config/loggingConfig";
import { computeNewExpiryDate, getChangeInWeeks, getRemainingWeeks } from '../helpers/sellerItem';
import { deductMappiBalance } from './membership.service';
import { MappiDeductionError } from '../errors/MappiDeductionError';

/* Helper Functions */
const buildDefaultSearchFilters = () => {
Expand Down Expand Up @@ -310,68 +313,104 @@ export const getAllSellerItems = async (
export const addOrUpdateSellerItem = async (
seller: ISeller,
item: ISellerItem
): Promise<ISellerItem | null> => {
): Promise<{ sellerItem: ISellerItem | null, consumedMappi: number }> => {
try {
const today = new Date();
logger.debug(`Seller data: ${seller.seller_id}`);

// Calculate expiration date based on duration (defaults to 1 week)
const duration = Number(item.duration) || 1;
const durationInMs = duration * 7 * 24 * 60 * 60 * 1000;
const expiredBy = new Date(today.getTime() + durationInMs);

logger.debug(`Seller data: ${ seller }`);

// Ensure unique identifier is used for finding existing items
const query = {
// Find existing item by _id and seller_id
const query = {
_id: item._id || undefined,
seller_id: seller.seller_id,
};

// Attempt to find the existing item
const existingItem = await SellerItem.findOne(query);

let consumedMappi = 0;
let savedItem: ISellerItem | null = null;

if (existingItem) {
// Update the existing item
// --- Update existing item ---
const changeInWeeks = getChangeInWeeks(existingItem, item);
logger.info(`Change in duration (weeks): ${changeInWeeks}`);

if (changeInWeeks !== 0) {
// Deduct/add mappi only if duration changes
await deductMappiBalance(seller.seller_id, changeInWeeks);
consumedMappi = -changeInWeeks;
}

// Compute new expiry date
const newExpiry = computeNewExpiryDate(existingItem, item);
logger.debug(`Computed new expiry date: ${newExpiry}`);

// Update fields
existingItem.set({
...item,
expired_by: expiredBy,
image: item.image || existingItem.image, // Use existing image if a new one isn't provided
duration: Math.max(Number(existingItem.duration) + changeInWeeks, 1),
expired_by: newExpiry,
image: item.image || existingItem.image,
price: item.price ?? existingItem.price,
stock_level: item.stock_level ?? existingItem.stock_level,
description: item.description ?? existingItem.description,
name: item.name ?? existingItem.name,
});
const updatedItem = await existingItem.save();

logger.info('Item updated successfully:', { updatedItem });
return updatedItem;
savedItem = await existingItem.save();
logger.info('Seller item updated successfully', { id: savedItem._id });
} else {
// --- Create new item ---
const now = new Date();
const duration = Math.max(Number(item.duration) || 1, 1);
const expiredBy = new Date(now.getTime() + duration * 7 * 24 * 60 * 60 * 1000);

// Deduct mappi for new item
await deductMappiBalance(seller.seller_id, duration);
consumedMappi = -duration;

// Ensure item has a unique identifier for creation
const newItemId = item._id || new mongoose.Types.ObjectId().toString();

// Create a new item
const newItem = new SellerItem({
_id: newItemId,
seller_id: seller.seller_id,
name: item.name ? item.name.trim() : '',
description: item.description ? item.description.trim() : '',
price: parseFloat(item.price?.toString() || '0.01'), // Ensure valid price
stock_level: item.stock_level || StockLevelType.AVAILABLE_1,
duration: parseInt(item.duration?.toString() || '1'), // Ensure valid duration
image: item.image,
name: item.name?.trim() ?? '',
description: item.description?.trim() ?? '',
price: item.price ?? 0.01,
stock_level: item.stock_level ?? StockLevelType.AVAILABLE_1,
duration,
image: item.image ?? null,
expired_by: expiredBy,
});

await newItem.save();
savedItem = await newItem.save();
logger.info('Seller item created successfully', { id: savedItem._id });
}

logger.info('Item created successfully:', { newItem });
return newItem;
return { sellerItem: savedItem, consumedMappi };
} catch (error: any) {
if (error instanceof MappiDeductionError) {
logger.error(`MappiDeductionError for piUID ${error.pi_uid}: ${error.message}`);
throw error;
} else {
logger.error(`Failed to add or update seller item for sellerID ${seller.seller_id}: ${error}`);
throw error;
}
} catch (error) {
logger.error(`Failed to add or update seller item for sellerID ${ seller.seller_id}: ${ error }`);
throw error;
}
};

// Delete existing seller item
export const deleteSellerItem = async (id: string): Promise<ISellerItem | null> => {
try {
const item = await SellerItem.findById(id).exec()
if (!item) {
logger.warn(`Seller item with ID ${ id } not found for deletion.`);
return null;
}

// refund mappi equivallent to remaining weeks if not 0
const remweeks = getRemainingWeeks(item)
await deductMappiBalance(item.seller_id, -remweeks);

const deletedSellerItem = await SellerItem.findByIdAndDelete(id).exec();
return deletedSellerItem ? deletedSellerItem as ISellerItem : null;
} catch (error) {
Expand Down
Loading