Last active
March 16, 2023 09:01
-
-
Save ImtiazChowdhury/1c4343d0191d83a535b6aa01719e3645 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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