Skip to content

Instantly share code, notes, and snippets.

@ImtiazChowdhury
Last active March 16, 2023 09:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ImtiazChowdhury/1c4343d0191d83a535b6aa01719e3645 to your computer and use it in GitHub Desktop.
Save ImtiazChowdhury/1c4343d0191d83a535b6aa01719e3645 to your computer and use it in GitHub Desktop.
const BaseOperations = require("../baseOperations/baseOperations")
const orderValidatorFactory = require("../../validator/orderValidator");
const orderFormatterFactory = require("../../formatter/orderFormatter");
const orderUpdateValidatorFactory = require("../../validator/orderUpdateValidator");
const orderUpdateFormatterFactory = require("../../formatter/orderUpdateFormatter");
const OrderDB = require('../../database/mongodb/order')
const { isValidId, validateId, convertToId } = require("../../database/mongodb/utils/util");
// const orderFilterValidatorFactory = require("../../validator/orderFilterValidator")
const ProductDB = require("../../database/mongodb/product")
const CustomerDB = require("../../database/mongodb/customer");
const getCityCodeByName = require("../../database/json/geo/getCityCodeByName");
const { OperationError } = require("../lib/util");
const getDeliveryCharge = require("../../database/json/geo/getDeliveryCharge");
const sendEventSMS = require("../../lib/sendEventSMS");
const { generateSerial } = require("../../lib/serialGenerator");
const withSessionTransaction = require("../../database/mongodb/withSession");
const orderAcceptValidatorFactory = require("../../validator/orderAcceptValidator");
const orderFullPaidValidatorFactory = require("../../validator/orderFullPaidValidator");
const ProducerDB = require("../../database/mongodb/producer");
const ProducerTransactionOperations = require("../producerTransaction");
const orderProductValidatorFactory = require("../../validator/orderProuctValidator");
const orderPricingFormatterFactory = require("../../formatter/orderPricingFormatter");
const RetailerDB = require("../../database/mongodb/retailer");
const orderPropsFormatterFactory = require("../../formatter/orderPropsFormatter");
const shipmentValidatorFactory = require("../../validator/shipmentValidator");
const orderStatus = require("../../config/orderStatus.json");
const sendNotification = require("../../lib/sendNotification");
const UnpaidOrderDB = require("../../database/mongodb/unpaidOrder");
const initiateOnlinePayment = require("../../lib/initiateSSLCommerzPayment");
const bkashUtils = require("../../lib/bkashPayment");
const walletMixUtils = require("../../lib/walletMixPayment");
const aamarpayUtils = require("../../lib/aamarpayPayment");
const paymentConfirmValidatorFactory = require("../../validator/paymentConfirmValidator");
const CustomerTransactionOperations = require("../customerTransaction");
const CustomerOperations = require("../customer");
const RetailerTransactionOperations = require("../retailerTransaction");
const RetailerOperations = require("../retailer");
const producerDB = new ProducerDB();
const productDB = new ProductDB();
const customerDB = new CustomerDB()
const retailerDB = new RetailerDB()
const customerOperations = new CustomerOperations();
const dbOps = new OrderDB()
const formatter = {
create: orderFormatterFactory({
convertToId,
getProductById: productDB.readOne,
getDeliveryCharge: getDeliveryCharge,
generateSerial: generateSerial,
getRetailerById: retailerDB.readOne,
}),
orderPricing: orderPricingFormatterFactory({
convertToId,
getProductById: productDB.readOne,
getDeliveryCharge: getDeliveryCharge,
generateSerial: generateSerial,
getRetailerById: retailerDB.readOne,
}),
orderProps: orderPropsFormatterFactory({
generateSerial: generateSerial,
}),
update: orderUpdateFormatterFactory({
convertToId,
getOrderById: dbOps.readOne,
getProductById: productDB.readOne,
getDeliveryCharge: getDeliveryCharge,
})
}
const validator = {
create: orderValidatorFactory({
isValidId,
getCustomerById: customerDB.readOne,
getProductById: productDB.readOne,
getCityCodeByName: getCityCodeByName,
getRetailerById: retailerDB.readOne,
}),
orderItems: orderProductValidatorFactory({
isValidId,
getProductById: productDB.readOne,
getCityCodeByName: getCityCodeByName,
getRetailerById: retailerDB.readOne,
}),
update: orderUpdateValidatorFactory({
isValidId,
getProductById: productDB.readOne,
getOrderById: dbOps.readOne,
getCustomerById: customerDB.readOne,
getCityCodeByName: getCityCodeByName,
}),
// list: orderFilterValidatorFactory({
// isValidId,
// }),
list: () => { },
detail: validateId,
remove: validateId,
acceptBidValidator: orderAcceptValidatorFactory({
isValidId,
getOrderById: dbOps.readOne,
getProductById: productDB.readOne,
}),
fullPaidValidator: orderFullPaidValidatorFactory({
isValidId,
getOrderById: dbOps.readOne,
}),
shipmentValidator: shipmentValidatorFactory({
isValidId,
getOrderById: dbOps.readOne,
}),
paymentConfirm: paymentConfirmValidatorFactory({})
}
const unpaidOrderDB = new UnpaidOrderDB()
const customerTransactionOperations = new CustomerTransactionOperations()
const retailerTransactionOperations = new RetailerTransactionOperations()
const retailerOperations = new RetailerOperations()
const producerTransactionOperations = new ProducerTransactionOperations()
class OrderOperations extends BaseOperations {
constructor() {
super(formatter, validator, dbOps)
}
async create(input) {
const errors = await this.validator.create(input);
if (errors) throw new OperationError(400, errors, "Invalid input");
const productErrors = await this.validator.orderItems(input);
if (productErrors) throw new OperationError(400, productErrors, "Invalid input");
const orderInfo = await this.formatter.create(input);
const orderPricing = await this.formatter.orderPricing(input);
let orderProps = {}
if (!input.preview) {
orderProps = orderProps = await this.formatter.orderProps(input, orderPricing);
}
const entity = { ...orderInfo, ...orderPricing, ...orderProps };
if (!input.retailer) { //customer order
//suggest nearby supplier for each shipment
for (let shipment of entity.shipment) {
const nearbySupplier = await retailerDB.findSupplier(input.deliveryLocation, entity, shipment);
shipment.nearbySupplier = nearbySupplier.data;
shipment.supplierConfirmed = false;
}
}
if (input.preview) {
return entity;
}
if (entity.advanceAmount > 0 && entity.paymentMethod === "cod") {
throw new OperationError(400, { paymentMethod: "Order requires advance payment, can not pay with Cash On Delivery" });
}
if (entity.paymentMethod === "pallabiPoints") {
const customerDetail = entity.customerType === "customer" ?
await customerDB.readOne(entity.customer)
: await retailerDB.readOne(entity.retailer);
const amountToPay = entity.paymentMode === "advance" ? entity.advanceAmount : entity.totalCharge;
const availableBalance = customerDetail.commissionBalance;
if (!availableBalance || availableBalance < amountToPay) {
throw new OperationError(400, { paymentMethod: "Not enough balance on your pallabi wallet" });
}
let writeResults;
if (entity.accepted && entity.type === "producerOrder") {
//for fixed price order, update product to sold
await productDB.updateOne(entity.product[0]._id, { sellStatus: "sold" });
}
writeResults = await dbOps.writeOne(entity);
if (entity.customerType === "customer") {
const transaction = await customerTransactionOperations.create({
customer: entity.customer?.toHexString(),
type: "orderCharge",
reason: `Payment with pallabi points for order ${entity.shortCode}`,
item: writeResults._id?.toHexString(),
amount: amountToPay,
})
//create transaction and approve immediately
const updatedTransaction = await customerTransactionOperations.update({
_id: transaction._id?.toHexString(),
approved: true
})
await dbOps.updateOne(writeResults._id, { paymentAmount: amountToPay })
writeResults.paymentInfo = { ...transaction, ...updatedTransaction };
writeResults.paymentAmount = amountToPay;
} else {
const transaction = await retailerTransactionOperations.create({
retailer: entity.retailer?.toHexString(),
type: "supplyCharge",
reason: `Payment with pallabi points for order ${entity.shortCode}`,
item: writeResults._id?.toHexString(),
amount: amountToPay,
})
//create transaction and approve immediately
const updatedTransaction = await retailerTransactionOperations.update({
_id: transaction._id?.toHexString(),
approved: true
})
await dbOps.updateOne(writeResults._id, { paymentInfo: { ...transaction, ...updatedTransaction }, paymentAmount: amountToPay })
writeResults.paymentInfo = { ...transaction, ...updatedTransaction, paymentAmount: amountToPay };
writeResults.paymentAmount = amountToPay;
}
if (writeResults.type !== "platformOrder") {
const productDetail = await productDB.readOne(writeResults.product[0]._id)
if (writeResults.type === "producerOrder") {
sendNotification(productDetail.producer, {
title: `New Order Received`,
body: `New order received for your product ${productDetail.title}`
})
} else if (writeResults.type === "producerBid") {
sendNotification(productDetail.producer, {
title: `New Bid Received`,
body: `New Bid of BDT ${writeResults.producerCommission} received for your product ${productDetail.title}`,
text: `Accept the bid if you are satisfied with the offered price`
})
}
}
OrderOperations.floatSingleOrder(writeResults)
return writeResults;
} else if (entity.paymentMethod !== "cod") {
const writeResults = await unpaidOrderDB.writeOne(entity);
let paymentUrl;
switch (entity.paymentMethod) {
case "sslCommerz":
paymentUrl = await initiateOnlinePayment(writeResults);
break;
case "bkash":
paymentUrl = await bkashUtils.createPaymentSession(writeResults);
break;
case "walletMix":
paymentUrl = await walletMixUtils.initiateOnlinePayment(writeResults);
break;
case "aamarpay":
paymentUrl = await aamarpayUtils.initiateOnlinePayment(writeResults);
break;
}
writeResults.paymentUrl = paymentUrl;
await unpaidOrderDB.updateOne(writeResults._id, { paymentUrl })
return writeResults;
} else {
let writeResults;
async function createOrderAndUpdateProductStatus() {
if (entity.accepted && entity.type === "producerOrder") { //for fixed price order, update product to sold
await productDB.updateOne(entity.product[0]._id, { sellStatus: "sold" });
}
writeResults = await dbOps.writeOne(entity);
}
await withSessionTransaction(createOrderAndUpdateProductStatus)
if (writeResults.type !== "platformOrder") {
const productDetail = await productDB.readOne(writeResults.product[0]._id)
if (writeResults.type === "producerOrder") {
sendNotification(productDetail.producer, {
title: `New Order Received`,
body: `New order received for your product ${productDetail.title}`
})
} else if (writeResults.type === "producerBid") {
sendNotification(productDetail.producer, {
title: `New Bid Received`,
body: `New Bid of BDT ${writeResults.producerCommission} received for your product ${productDetail.title}`,
text: `Accept the bid if you are satisfied with the offered price`
})
}
}
if (entity.accepted) {
if (!input.retailer) {
const customer = await customerDB.readOne(input.customer);
// sendEventSMS("orderPlaced", { customer, order: entity }, customer.phone).catch(console.log);
} else {
const retailer = await retailerDB.readOne(input.retailer);
// sendEventSMS("orderPlacedRetailer", { retailer, order: entity }, retailer.phone).catch(console.log);
}
}
OrderOperations.floatSingleOrder(writeResults)
return writeResults;
}
}
async refund(input) {
if (!isValidId(input._id)) throw new OperationError({ _id: "invalid Id" });
const orderDetail = await dbOps.readOne(input._id);
if (!orderDetail) throw new OperationError({ _id: "no order with given id" });
if (orderDetail.paymentMethod === "bkash" && orderDetail.paymentInfo) {
return await bkashUtils.refundTransaction(orderDetail)
}
}
async acceptBid(input) {
const errors = await this.validator.acceptBidValidator(input);
if (errors) throw new OperationError(400, errors, "Invalid input");
const order = await this.dbOps.readOne(input._id);
const orderPropsToUpdate = {
accepted: !!input.accepted,
}
if (!input.accepted) {
orderPropsToUpdate.status = "pending"
}
if (!!input.accepted) {
orderPropsToUpdate.status = "processing";
orderPropsToUpdate.acceptedAt = new Date();
}
const orderWithSameProduct = await this.list({ product: [order.product[0]._id] }, {}, { limit: 1000000000 })
const otherOrder = orderWithSameProduct?.data?.filter(i => i._id?.toHexString() !== order._id?.toHexString());
await withSessionTransaction(async () => {
//update order accepted
await this.dbOps.updateOne(input._id, orderPropsToUpdate);
for (let item of otherOrder) {
// refund advance amount to wallet
if (item.paymentAmount) {
if (!item.refundPaid) {
const transaction = {
amount: item.paymentAmount,
type: "creditPlus",
reference: {
type: "order",
_id: item._id?.toHexString()
},
reason: "Advance refunded against order " + item.shortCode,
}
if (item.customerType === "customer") {
const transactionResult = await customerTransactionOperations.create({
customer: item.customer?.toHexString(),
...transaction
})
await customerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true
})
} else {
const transactionResult = await retailerTransactionOperations.create({
retailer: item.retailer?.toHexString(),
...transaction
})
await retailerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true
})
}
}
}
await this.dbOps.updateOne(item._id, { status: "cancelled", refundPaid: true })
}
if (!!input.accepted) {
await productDB.updateOne(order.product[0]._id, { sellStatus: "sold", acceptedAt: new Date() });
} else {
await productDB.updateOne(order.product[0]._id, { sellStatus: "unsold" });
}
})
if (!!input.accepted) {
sendNotification(order.customer || order.retailer, {
title: "🎉 Bid accepted",
body: `Your bid #${order.shortCode} has been accepted by the producer. Your delivery will be dispatched soon.`,
})
}
return {
accepted: !!input.accepted
};
}
async payProducer(input) {
return {}
}
static async cancelDepositTimeoutOrder() {
//orders accepted 4 hours ago and not deposited yet will get cancelled and products will revert back to unsold
const dateNow = new Date(Date.now() - 1000 * 60 * 60 * 4)
await dbOps.cancelOrderWithNoDeposit(dateNow);
await productDB.setNoDepositProductUnsold(dateNow);
}
static async floatOrder() {
//order requests that haven't been
const unconfirmedOrders = await dbOps.readUnconfirmedOrders();
for (let order of unconfirmedOrders) {
await OrderOperations.floatSingleOrder(order)
}
}
static async floatSingleOrder(order) {
const dateNow = new Date()
for (let shipment of order.shipment) {
if (shipment.supplierConfirmed) continue;
if (shipment.requestTimeout && (shipment.requestTimeout < dateNow)) { //supplier didn't confirm; move to next
if (shipment.retailerProduct) {
const supplierDetail = await retailerDB.readOne(shipment.supplier);
if (shipment.supplierCancelled) continue;
shipment.supplierMissed = true;
sendNotification(order.customer, {
title: "Shipment could not be accepted",
body: `Shipment ${shipment.id} of your order ${order.shortCode} was not accepted by ${supplierDetail?.businessName || supplierDetail?.name}`,
})
if ((shipment.advanceAmount || order.paymentAmount === order.totalCharge) && !shipment.refundPaid) {
const transaction = {
customer: order.customer?.toHexString(),
amount: order.paymentAmount === order.totalCharge ? shipment.totalCharge : shipment.advanceAmount,
type: "creditPlus",
reason: `Refund of paid amount for shipment ${shipment.id} of order ${order.shortCode}`,
reference: {
type: "order",
_id: order._id?.toHexString(),
}
}
const transactionResult = await customerTransactionOperations.create(transaction);
await customerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true,
})
shipment.refundPaid = true;
}
continue;
}
if (shipment.supplierCancelled) { //previous request was rejected
shipment.rejectedSuppliers = Array.isArray(shipment.rejectedSuppliers) ? [...shipment.rejectedSuppliers, shipment.supplier] : [shipment.supplier]
} else { //previous request was missed
shipment.missedSupplier = Array.isArray(shipment.missedSupplier) ? [...shipment.missedSupplier, shipment.supplier] : [shipment.supplier]
}
const rejectedAndMissedSuppliers = [];
if (Array.isArray(shipment.rejectedSuppliers)) shipment.rejectedSuppliers.forEach(i => rejectedAndMissedSuppliers.push(i?.toHexString()))
if (Array.isArray(shipment.missedSupplier)) shipment.missedSupplier.forEach(i => rejectedAndMissedSuppliers.push(i?.toHexString()))
shipment.supplier = null;
if (Array.isArray(shipment.nearbySupplier)) {
for (let supplier of shipment.nearbySupplier) {
if (rejectedAndMissedSuppliers.includes(supplier._id.toHexString())) continue;
else {
shipment.supplier = supplier._id;
}
}
}
} else if (shipment.requestTimeout && (shipment.requestTimeout > dateNow)) {
//already floating and not timed out yet
return;
}
//otherwise the order is new, not floated yet.
if (!shipment.supplier) { //no supplier accepted; end of list
sendNotification(order.customer, {
title: "Merchant could not be found",
body: `Shipment ${shipment.id} of your order ${order.shortCode} was not accepted by any merchant, our represantative will contact you for alternative delivery arrangement`,
})
shipment.supplierCancelled = false;
shipment.supplierConfirmed = true;
shipment.deliveryTime = "2 Days";
shipment.totalCharge = shipment.itemCharge;
shipment.type = "shipFromWarehouse";
} else { //next supplier available; float again
const supplierDetail = await retailerDB.readOne(shipment.supplier);
if (supplierDetail) {
//set supplier specific fields
shipment.supplierCancelled = false;
shipment.requestTimeout = new Date(Date.now() + 1000 * 60) // 1 minute
shipment.deliveryTime = shipment.type === "schedule" ? shipment.deliveryTime : (supplierDetail.deliveryTime || "30 Minutes");
shipment.deliveryCharge = supplierDetail.deliveryCharge || 0;
shipment.totalCharge = shipment.itemCharge + supplierDetail.deliveryCharge;
console.log(`Floating: shipment ${shipment.id} of order ${order.shortCode} to ${supplierDetail.businessName}`)
sendNotification(
shipment.supplier,
{
title: "Incoming Order",
body: "Incoming order of BDT " + Math.round(shipment.totalCharge),
type: "incomingOrder",
},
"incomingOrder"
)
}
}
}
//update db
const orderId = order._id;
delete order._id;
await dbOps.updateOne(orderId, order);
}
async updateShipment(input) {
const errors = await this.validator.shipmentValidator(input);
if (errors) throw new OperationError(400, errors, "Invalid input");
const order = await this.dbOps.readOne(input._id);
for (let id of input.shipment) {
const shipment = order.shipment.find(s => s.id === id);
if (shipment) {
if (order.customerType === "customer") {
const customerDetail = await customerDB.readOne(order.customer);
const supplierDetail = await retailerDB.readOne(shipment.supplier);
//* supplier status update
if (input.deliveryStatus) {
shipment.deliveryStatus = input.deliveryStatus?.trim()
if (customerDetail && supplierDetail && input.deliveryStatus === "sentForDelivery") {
// sendEventSMS("orderAccepted", { customer: customerDetail, supplier: supplierDetail, order: order }, customerDetail.phone)
sendNotification(customerDetail._id, {
title: "Shipment Dispatched",
body: `Shipment ${shipment.id} of your order ${order.shortCode} has been sent for delivery by ${supplierDetail.businessName || supplierDetail.name}, you will receive the delivery soon`,
})
}
if (customerDetail && supplierDetail && input.deliveryStatus === "delivered") {
// sendEventSMS("orderAccepted", { customer: customerDetail, supplier: supplierDetail, order: order }, customerDetail.phone)
sendNotification(customerDetail._id, {
title: "Shipment Delivered",
body: `Shipment ${shipment.id} of your order ${order.shortCode} has been delivered by ${supplierDetail.businessName || supplierDetail.name}`,
url: `/Root/profileStack/orderDetail?order=${order._id}`
})
}
}
//* customer status update
if (input.receiveStatus) {
shipment.receiveStatus = input.receiveStatus?.trim()
if (supplierDetail) {
if (!shipment.commissionPaid && input.receiveStatus === "received" && order.type === "platformOrder") {
try {
if (order.paymentMethod !== "cod") {
const amount = order.paymentMode === "advance" ? shipment.advanceAmount : shipment.totalCharge;
// pay retailer if not paid on delivery
const transaction = {
retailer: shipment.supplier?.toHexString(),
type: "orderPayment",
amount,
reason: `Payment for shipment ${shipment.id} of order ${order.shortCode}`,
reference: {
type: "order",
_id: order._id?.toHexString(),
}
}
const transactionResult = await retailerTransactionOperations.create(transaction)
await retailerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true
})
}
// deduct commission from retailer account balance
const transaction = {
retailer: shipment.supplier?.toHexString(),
type: "orderCommission",
amount: shipment.totalCharge * (supplierDetail.commissionPercentage || 5) / 100,
reason: `Platform commission for shipment ${shipment.id} of order ${order.shortCode}`,
reference: {
type: "order",
_id: order._id?.toHexString(),
}
}
const transactionResult = await retailerTransactionOperations.create(transaction)
await retailerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true
})
shipment.commissionPaid = true;
//disapprove retailer account if credit balance below grace amount
if (supplierDetail.commissionBalance && supplierDetail.commissionBalance < 0 - (supplierDetail.graceAmount || 1500)) {
retailerOperations.update({
_id: supplierDetail._id?.toHexString(),
approved: false
})
}
} catch (err) {
console.log(err.list)
}
}
}
}
if ("acceptReorder" in input) {
if (order.customer?.toHexString() !== input.customer) throw new OperationError(400, { supplier: "Can not process request" })
if (!shipment.reorderAvailable) throw new OperationError(400, { supplier: "Order is not available for re-ordering" })
shipment.reorderAvailable = false;
shipment.supplierCancelled = true;
}
if ("supplierDismissed" in input) {
if (shipment.supplier?.toHexString() !== input.supplier) throw new OperationError(400, { supplier: "Can not process request" })
shipment.supplierCancelled = true;
shipment.supplierDismissed = true;
}
if ("reorderAvailable" in input) {
if (shipment.supplier?.toHexString() !== input.supplier) throw new OperationError(400, { supplier: "Can not process request" })
shipment.reorderAvailable = true;
sendNotification(customerDetail._id, {
title: "Re-order request",
body: `${supplierDetail.businessName || supplierDetail.name} requested you to re-order shipment ${shipment.id} of your order ${order.shortCode} `,
})
}
//* supplier shipment acceptance update
if ("supplierConfirmed" in input) {
const dateNow = new Date();
if (shipment.requestTimeout < dateNow && !input.forceConfirm) {
throw new OperationError(400, { timeout: "You did not respond in time" })
}
if (input.supplierConfirmed) {
shipment.supplierConfirmed = true;
shipment.riderRequired = !!input.riderRequired;
if (customerDetail && supplierDetail) {
// sendEventSMS("orderAccepted", { customer: customerDetail, supplier: supplierDetail, order: order }, customerDetail.phone)
sendNotification(customerDetail._id, {
title: "Order Accepted",
body: `Shipment ${shipment.id} of your order ${order.shortCode} has been accepted by ${supplierDetail.businessName || supplierDetail.name}`
})
}
} else {
if (customerDetail && supplierDetail) {
if (shipment.retailerProduct) {
const rejectMsg = input.message ? `Reason: ${input.message.trim()}` : ""
sendNotification(customerDetail._id, {
title: "Shipment rejected",
body: `Shipment ${shipment.id} of your order ${order.shortCode} was not accepted by ${supplierDetail.businessName || supplierDetail.name}. ${rejectMsg}`,
})
if ((shipment.advanceAmount || order.paymentAmount === order.totalCharge) && !shipment.refundPaid) {
const transaction = {
customer: order.customer?.toHexString(),
amount: order.paymentAmount === order.totalCharge ? shipment.totalCharge : shipment.advanceAmount,
type: "creditPlus",
reason: `Refund of paid amount for shipment ${shipment.id} of order ${order.shortCode}`,
reference: {
type: "order",
_id: order._id?.toHexString(),
}
}
const transactionResult = await customerTransactionOperations.create(transaction);
await customerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true,
})
shipment.refundPaid = true;
}
}
}
shipment.supplierCancelled = true;
}
}
} else if (order.customerType === "retailer") {
const retailerDetail = await retailerDB.readOne(order.retailer);
//* channel partner status update
if (input.deliveryStatus) {
shipment.deliveryStatus = input.deliveryStatus?.trim()
if (retailerDetail && input.deliveryStatus === "sentForDelivery") {
// sendEventSMS("orderAccepted", { customer: retailerDetail, supplier: supplierDetail, order: order }, retailerDetail.phone)
sendNotification(retailerDetail._id, {
title: "Shipment Dispatched",
body: `Shipment ${shipment.id} of your order ${order.shortCode} has been sent for delivery, you will receive the delivery soon`,
})
}
if (retailerDetail && input.deliveryStatus === "delivered") {
// sendEventSMS("orderAccepted", { customer: retailerDetail, supplier: supplierDetail, order: order }, retailerDetail.phone)
sendNotification(retailerDetail._id, {
title: "Shipment Delivered",
body: `Shipment ${shipment.id} of your order ${order.shortCode} has been delivered`,
url: `/Root/profileStack/orderDetail?order=${order._id}`
})
}
}
//* retailer status update
if (input.receiveStatus) {
if (input.receiveStatus?.trim() === "received") {
//deduct account balance if retailer order
if (!shipment.balanceDeducted) {
const balanceToDeduct = shipment.totalCharge - (shipment.advanceAmount || 0)
if (retailerDetail.commissionBalance < balanceToDeduct) {
throw new OperationError(400, { balance: "Insufficient balance" })
} else {
const transaction = {
retailer: retailerDetail._id?.toHexString(),
type: "supplyCharge",
amount: balanceToDeduct,
reason: `Balance deduction on receiving shipment ${shipment.id} of supply ${order.shortCode}`,
reference: {
type: "order",
_id: order._id?.toHexString(),
}
}
const transactionResult = await retailerTransactionOperations.create(transaction)
await retailerTransactionOperations.update({
_id: transactionResult._id?.toHexString(),
approved: true
})
shipment.balanceDeducted = true;
}
}
}
shipment.receiveStatus = input.receiveStatus?.trim()
}
}
if (order.type === "producerOrder" || order.type === "producerBid") {
if (!shipment.commissionPaid && order.product && order.product[0]) {
const productDetail = await productDB.readOne(order.product[0]._id)
if (productDetail.producer) {
const transaction = {
producer: productDetail.producer?.toHexString(),
type: "commissionPlus",
amount: order.producerCommission,
reason: `Payment for order ${order.shortCode} of product ${productDetail.title}`,
item: order._id?.toHexString()
}
//no need to auto approve, admin will approve manually
const transactionResult = await producerTransactionOperations.create(transaction)
shipment.commissionPaid = true;
}
}
}
}
}
//mark order as delivered if all shipments delivered
let allShipmentDelivered = true;
let allShipmentReceived = true;
for (let shipment of order.shipment) {
if (shipment.deliveryStatus === "pending") {
allShipmentDelivered = false
}
if (shipment.receiveStatus === "pending") {
allShipmentReceived = false;
}
if (!allShipmentDelivered && !allShipmentReceived) break;
}
if (allShipmentDelivered) {
order.delivered = true;
}
if (allShipmentReceived) {
order.received = true;
order.status = orderStatus[6].code;
}
delete order._id;
await this.dbOps.updateOne(input._id, order);
return order;
}
async fulfillOrderSslCommerz(input) {
const orderId = input.tran_id;
if (orderId) {
const orderDetail = await unpaidOrderDB.readOne(orderId);
if (!orderDetail) throw new OperationError(400, { tran_id: "no order with this transaction id" }, "IPN error")
const validationResponse = await initiateOnlinePayment.validateOnlinePayment(input);
if (validationResponse && validationResponse.status === "VALID") {
let writeResults;
async function createOrderAndUpdateProductStatus() {
orderDetail.paymentAmount = parseFloat(validationResponse.amount);
orderDetail.paymentInfo = validationResponse;
if (orderDetail.accepted && orderDetail.type === "producerOrder") { //for fixed price order, update product to sold
await productDB.updateOne(orderDetail.product[0]._id, { sellStatus: "sold" });
}
writeResults = await dbOps.writeOne(orderDetail);;
}
await withSessionTransaction(createOrderAndUpdateProductStatus)
const customer = await customerDB.readOne(orderDetail.customer);
// sendEventSMS("orderPlaced", { customer, order: orderDetail }, customer.phone);
// set customer as verified with first successful online transaction
if (customer && !customer.verified) {
await customerOperations.update({ _id: customer._id, verified: true })
}
if (orderDetail.type !== "platformOrder") {
const productDetail = await productDB.readOne(orderDetail.product[0]._id)
if (orderDetail.type === "producerOrder") {
sendNotification(productDetail.producer, {
title: `New Order Received`,
body: `New order received for your product ${productDetail.title}`
})
} else if (orderDetail.type === "producerBid") {
sendNotification(productDetail.producer, {
title: `New Bid Received`,
body: `New Bid of BDT ${orderDetail.producerCommission?.toFixed(2)} received for your product ${productDetail.title}`,
text: `Accept the bid if you are satisfied with the offered price`
})
}
}
OrderOperations.floatSingleOrder(writeResults)
return writeResults;
} else {
throw new OperationError(400, { tran_id: "invalid ipn" }, "IPN error")
}
} else {
throw new OperationError(400, { tran_id: "no order id" }, "IPN error")
}
}
async confirmPayment(input) {
this.checkParamType("input", input, ["object", "array"])
const errors = await this.validator.paymentConfirm(input);
if (errors) throw new OperationError(400, errors, "Invalid input");
let response;
switch (input.walletType) {
case "bkash":
response = await bkashUtils.executePaymentSession(input);
break;
case "walletMix":
response = await walletMixUtils.validateOnlinePayment(input);
break;
case "aamarpay":
response = await aamarpayUtils.validateOnlinePayment(input);
break;
}
// set customer as verified with first successful online transaction
const customer = response.order?.customer;
if (customer) {
const customerDetail = await customerDB.readOne(customer);
if (customerDetail && !customerDetail.verified && response.success) {
await customerOperations.update({ _id: customer, verified: true })
}
// sendEventSMS("orderPlaced", { customer: customerDetail, order: response.order }, customerDetail.phone);
}
return response;
}
}
setInterval(OrderOperations.cancelDepositTimeoutOrder, 1000 * 60 * 5); //run every 5 minute
setInterval(OrderOperations.floatOrder, 1000 * 70); //run every 70 seconds
setTimeout(OrderOperations.floatOrder, 1000)
module.exports = OrderOperations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment