'use strict'; const { CrudService } = require('naf-framework-mongoose-free/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; const _ = require('lodash'); const assert = require('assert'); const { ObjectId } = require('mongoose').Types; const moment = require('moment'); const Transaction = require('mongoose-transactions'); // class AfterSaleService extends CrudService { constructor(ctx) { super(ctx, 'aftersale'); this.model = this.ctx.model.Trade.AfterSale; this.orderDetailModel = this.ctx.model.Trade.OrderDetail; this.orderModel = this.ctx.model.Trade.Order; this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec; this.configModel = this.ctx.model.System.Config; this.orderDetailService = this.ctx.service.util.orderDetail; this.tran = new Transaction(); } /** * 售后申请 ** 售后申请有几种:取消订单; 拒收; 退款; 退货; 换货 ** 取消订单:发货之前,付款之后: 买家申请, 卖家同意之后,直接处理退款即可(退回库存,券影响的所有商品全额退款,则退券) ** 拒收: 发货之后,收货之前;买家申请,卖家同意. 直到卖家 确认结束 售后,钱才退回给买家;(退回库存,券影响的所有商品全额退款,则退券) ** 退款:收货之后;买家申请,卖家同意.当即退钱,自动结束(退钱,券影响的所有商品全额退款,则退券) ** 退货:收货之后;买家申请,卖家同意.直到卖家 确认结束 售后,钱才退回给买家(退钱,退库存,券影响的所有商品全额退款,则退券) ** 换货:收货之后;买家申请,卖家同意.双方填写各自的单号. 买卖双方确认收货 后 售后结束(没什么变化?库存-x?) * @param {Object} body 售后信息 */ async create(body) { const { order_detail, goods: goods_id, set_id, type, ...others } = body; if (!order_detail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); // 查看该商品是否已经申请售后 let goods; const hasData = await this.model.count({ order_detail, set_id, 'goods._id': goods_id, status: { $nin: [ '!1', '!2', '!3', '!4', '!5' ] } }); if (hasData > 0) throw new BusinessError(ErrorCode.DATA_EXISTED, '该商品已有正在处理中的售后申请.请勿重复申请'); if (type !== '4' && type !== '5') { const { goods: goodsList } = orderDetail; goods = goodsList.find(f => { const { is_set = '1' } = f; if (!set_id && is_set === '1') return ObjectId(f._id).equals(goods_id); return set_id === f.set_id; }); // 套装的数据处理 if (set_id && _.get(goods, 'set_id') === set_id) { const { buy_num } = goods; const setData = goods.goods.find(f => _.get(f, 'spec._id') === goods_id); if (setData) { const { spec, goods: sg } = setData; const newGoods = { ...spec, buy_num, goods: sg }; goods = newGoods; } } if (!goods) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未在当前订单中搜索到要售后的商品'); } else { const realPay = await this.ctx.service.util.orderDetail.computedRealPay(orderDetail); others.money = realPay; others.desc = type === '4' ? '取消订单' : '拒收商品'; } const { shop, customer } = orderDetail; const apply_time = moment().format('YYYY-MM-DD HH:mm:ss'); this.orderDetailService.turn$numberDecimalToNumber(goods); const obj = { order_detail, customer, shop, goods, type, ...others, apply_time, status: '0', set_id }; const res = await this.model.create(obj); const msgData = { source_id: res._id, type: '0' }; try { await this.ctx.service.shop.shopNotice.remindToAfterSale(msgData); } catch (error) { const errobj = error; await this.ctx.service.util.email.errorEmail(errobj); } } async update(filter, update) { assert(filter); assert(update); const { _id, id } = filter; if (_id || id) filter = { _id: ObjectId(_id || id) }; // 检查数据是否存在 const entity = await this.model.findOne(filter).exec(); if (!entity) throw new BusinessError(ErrorCode.DATA_NOT_EXIST); try { // 先处理数据,数据处理对了,需要退钱再退钱 const { type } = entity; const { status } = update; // 没有修改状态,那就直接走修改返回 if (!status) { entity.set(update); await entity.save(); return; } // 退款信息,有内容就是要退款,没有内容(undefined)就是不需要退款 let refundInfo; // 要修改成的状态 const uStatus = _.get(update, 'status'); // 根据类型不同,处理的函数不一样. if (type === '1') { // 仅退款,退优惠券,退款 // 如果不是处理中,则不进行退款 if (_.get(update, 'status') === '1') { // 1.检验并组织退款信息 refundInfo = await this.toReturnMoney(entity, update, this.tran); // 2.检查是否退优惠券,该退就退 await this.toReturnCoupons(entity, update, this.tran); // 3.修改数据, 直接修改成退完款的状态.如果后面退款失败了.直接回滚了 update.status = '-1'; update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (_.get(update, 'status') === '!1') { update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } // 4.修改订单状态 await this.refundOrder(entity, this.tran); } else if (type === '2') { // 退货(退库存),退款,退优惠券 // 做记录,但是不是结束状态,都不退 if (uStatus === '-2') { // 1.检验并组织退款信息 refundInfo = await this.toReturnMoney(entity, update, this.tran); // 2.检查是否退优惠券 await this.toReturnCoupons(entity, update, this.tran); // 3.修改订单状态 await this.refundOrder(entity, this.tran); // 结束时间 update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (uStatus === '!2') { update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } } else if (type === '3') { // 换货,不需要退款 // 但需要检查买卖双方的快递是否签收,都签收的话,需要将状态改为结束 const uto = _.get(update, 'transport', {}); const eto = _.get(entity, 'transport', {}); // 有关快递的字段整合,将传来的和以前的放在一起.然后找是否签收 const to = { ...eto, ...uto }; const cr = _.get(to, 'customer_receive'); const sr = _.get(to, 'shop_receive'); if (cr && sr) { update.status = '-3'; update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (uStatus === '!3') { update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } } else if (type === '4') { // 取消订单: 退钱,退货(退库存)----不过不需要有快递信息,没发货,退优惠券 // 没有商品,只有拆分的订单号,用这个去退 if (uStatus === '4') { refundInfo = await this.returnOrder(entity, this.tran); update.status = '-4'; } update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (type === '5') { // 拒收: 退钱, 退货 // 如果状态不是完成,那就不退,只是数据修改 if (uStatus === '-5') { refundInfo = await this.returnOrder(entity, this.tran); update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (uStatus === '!5') { update.end_time = moment().format('YYYY-MM-DD HH:mm:ss'); } } else throw new BusinessError(ErrorCode.DATA_INVALID, '未知的售后类型,无法处理'); // 售后处理人的添加 if (update.status !== '0') { const admin = this.ctx.admin; update.deal_person = admin._id; } // 修改数据 this.tran.update('AfterSale', entity._id, update); await this.tran.run(); // 退钱 if (!refundInfo) return; // console.log(refundInfo); const res = await this.ctx.service.trade.pay.refund(refundInfo); if (res.errcode && res.errcode !== 0) throw new BusinessError(ErrorCode.SERVICE_FAULT, res.errmsg); } catch (error) { console.error(error); await this.tran.rollback(); throw new BusinessError(ErrorCode.SERVICE_FAULT, '售后:修改失败'); } finally { this.tran.clean(); } } /** * 售后退钱 **组织退钱的请求参数即可,没啥需要改的 * @param {Object} data 售后修改前的数据 * @param {Object} update 要修改的数据 * @param {Transaction} tran 数据库事务 */ async toReturnMoney(data, update, tran) { const { order_detail, goods, set_id } = data; const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); // 取出订单详情的每种商品规格的价格明细 const moneyDetail = this.ctx.service.util.orderDetail.moneyDetail(orderDetail); // 根据订单类型使用价格的key const priceKey = 'grp'; let returnMoney = 0; let goodsRealPay = 0; if (!set_id) { // 商品实际支付的金额 goodsRealPay = _.get(moneyDetail, `${goods._id}.${priceKey}`); } else { // 商品实际支付的金额 goodsRealPay = _.get(moneyDetail, `${set_id}.${priceKey}`); } // 需要退还的金额,如果传来的数据有金额,就使用传来的,没有的话就用原来的 returnMoney = _.get(update, 'money', _.get(data, 'money')); if (goodsRealPay < returnMoney) throw new BusinessError(ErrorCode.DATA_INVALID, '退款金额超出该商品支付的金额'); // 组成退款单号 const { order: order_id } = orderDetail; const order = await this.orderModel.findById(order_id); if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到支付订单信息'); const order_no = _.get(order, 'pay.pay_no'); if (!order_no) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到微信支付订单号'); const str = this.ctx.service.util.trade.createNonceStr(); const out_refund_no = `${order_no}-r-${str}`; const refundInfo = { reason: _.get(data, 'desc', '购物退款'), money: returnMoney, order_no, out_refund_no }; // 退积分 await this.ctx.service.user.point.refundOrderPoint(order_detail, tran); // 退商铺的入账流水金额 await this.ctx.service.shop.shopInBill.createByAfterSale(data, returnMoney, tran); await this.ctx.service.user.cashBack.refund(data, tran); return refundInfo; } /** * 售后退优惠券 **回溯至支付订单,检查每张优惠券影响的商品 及 当前售后的商品 是否都 全额退款 **如果优惠券影响的商品全额退款,那就退还优惠券; **如果有一个没有退款 或者 全额退款,那优惠券就不退 * @param {Object} data 售后修改前的数据 * @param {Object} update 要修改的数据 * @param {Transaction} tran 数据库事务 */ async toReturnCoupons(data, update, tran) { const orderDetail = await this.orderDetailModel.findById(data.order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); const { order: order_id } = orderDetail; const order = await this.orderModel.findById(order_id); if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到支付订单信息'); const odList = await this.orderDetailModel.find({ order: order_id }); // 已退款记录 const goodsRefundList = []; // 在下面处理已退款记录中,应该把当前的这个售后项,作为已处理售后 const tasq = { order_detail: data.order_detail, 'goods._id': _.get(data, 'goods._id'), status: [ '-1', '-2' ] }; const asdd = JSON.parse(JSON.stringify(data)); if (_.get(update, 'money')) asdd.money = _.get(update, 'money'); const tr = await this.checkIsAllRefund(data); // 如果当前商品的售后都不满足全额退款条件.那就别往后查了 if (tr) goodsRefundList.push(tasq); else return; for (const od of odList) { const { goods, _id: order_detail } = od; // 组合成查售后的条件 // 然后查这些商品有没有退款审核成功的记录, 且只有退全款的商品才能退券 const afterSaleQuerys = goods.map(i => ({ order_detail, 'goods._id': i._id, status: [ '-1', '-2' ] })); for (const asq of afterSaleQuerys) { const asd = await this.model.findOne(asq); if (asd) { const r = await this.checkIsAllRefund(asd); if (r) goodsRefundList.push(asq); } } } // 获取支付单的优惠明细 const dd = _.get(order, 'total_detail.discount_detail', {}); for (const uc_id in dd) { // uc_id 用户领取优惠券的id // 该优惠券影响的商品id列表 const goodsIds = Object.keys(_.get(dd, uc_id, {})); // 然后在已退款/退货记录中找,这个优惠券影响的商品是否都退款了.退全额 const r = goodsIds.every(i => goodsRefundList.find(f => i === f['goods._id'])); if (r) { // 说明这个优惠券影响的商品都退了,这个优惠券也就能退了 tran.update('UserCoupon', uc_id, { status: '0' }); } } } /** * 售后:修改拆分订单 * @param {Object} data 售后修改前的数据 * @param {Transaction} tran 数据库事务 */ async refundOrder(data, tran) { const { order_detail } = data; const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); // 获取拆分订单的数据,并找到商品列表 const goodsList = _.get(orderDetail, 'goods'); // 依次找这些商品是否都售后完成,都售后完成,就改订单状态 const asList = await this.model.find({ order_detail, status: { $nin: [ '0', '!1', '!2', '!3', '!4', '!5' ] } }); let status; let fl = []; // 将当前数据添加进去 fl.push({ goods: _.get(data, 'goods._id'), status: 'finish' }); for (const gs of goodsList) { const { set_id } = gs; if (!set_id) { const r = asList.find(f => ObjectId(_.get(f, 'goods._id')).equals(_.get(gs, '_id'))); if (r) { const finishList = [ '-1', '-2', '-3', '-4', '-5' ]; if (finishList.includes(r.status)) fl.push({ goods: gs._id, status: 'finish' }); } } else { const { goods } = gs; for (const i of goods) { const { spec } = i; const goods_id = _.get(spec, '_id'); const r = asList.find(f => f.set_id === set_id && _.get(f, 'goods._id') === goods_id); if (r) { const finishList = [ '-1', '-2', '-3', '-4', '-5' ]; if (finishList.includes(r.status)) fl.push({ set_id, goods: goods_id, status: 'finish' }); } } } } fl = _.uniqBy(fl, 'goods'); // 说明所有的商品 都有 已处理的售后 if (fl.length === goodsList.length) status = '-4'; // 有状态码,则修改订单的状态 if (status) { tran.update('OrderDetail', order_detail, { status }); } } /** * 售后 取消订单 & 拒收处理完成 * @param {Object} data 售后修改前的数据 * @param {Transaction} tran 数据库事务 */ async returnOrder(data, tran) { // 1.退钱 const { order_detail } = data; const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); const order_id = _.get(orderDetail, 'order'); const goodsList = _.get(orderDetail, 'goods'); const order = await this.orderModel.findById(order_id); if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到支付订单信息'); this.tran.update('OrderDetail', order_detail, { status: '-1' }); // 实付的钱 const realPay = await this.ctx.service.util.orderDetail.computedRealPay(orderDetail); // 组成退款单号 const str = this.ctx.service.util.trade.createNonceStr(); const order_no = _.get(order, 'pay.pay_no'); const out_refund_no = `${order_no}-r-${str}`; const refundInfo = { reason: _.get(data, 'desc'), money: realPay, order_no, out_refund_no }; // 2.退卷 // 查找支付订单拆分的所有订单.不包含当前要退的单.默认当前要退的单是全都退了的,只检查其他拆分的就可以 const odList = await this.orderDetailModel.find({ order: order_id, _id: { $ne: ObjectId(order_detail) } }); const goodsRefundList = []; // 将这个单内的商品加入满足全额退款的商品列表中 for (const g of goodsList) { const obj = { order_detail, 'goods._id': g._id }; goodsRefundList.push(obj); } for (const od of odList) { const { goods, _id: order_detail } = od; // 组合成查售后的条件 // 然后查这些商品有没有退款审核成功的记录, 且只有退全款的商品才能退券 const afterSaleQuerys = goods.map(i => ({ order_detail, 'goods._id': i._id, status: [ '-1', '-2' ] })); for (const asq of afterSaleQuerys) { const asd = await this.model.findOne(asq); if (asd) { const r = await this.checkIsAllRefund(asd); if (r) goodsRefundList.push(asq); } } } // 获取支付单的优惠明细 const dd = _.get(order, 'total_detail.discount_detail', {}); for (const uc_id in dd) { // uc_id 用户领取优惠券的id // 该优惠券影响的商品id列表 const goodsIds = Object.keys(_.get(dd, uc_id, {})); // 然后在已退款/退货记录中找,这个优惠券影响的商品是否都退款了.退全额 const r = goodsIds.every(i => goodsRefundList.find(f => i === f['goods._id'])); if (r) { // 说明这个优惠券影响的商品都退了,这个优惠券也就能退了 tran.update('UserCoupon', uc_id, { status: '0' }); } } // 3.退库存 for (const g of goodsList) { const { _id, buy_num } = g; const goodsSpec = await this.goodsSpecModel.findById(_id); if (!goodsSpec) continue; const { num } = goodsSpec; const newNum = this.ctx.plus(buy_num, num); this.tran.update('GoodsSpec', goodsSpec._id, { num: newNum }); } return refundInfo; } /** * 判断是否全额退的商品 * @param {Object} data 售后数据 * @return {Boolean} */ async checkIsAllRefund(data) { // 商品有退款审核通过的记录,查询每个商品是否退的是全款 // money: 实际退款的金额 const { money } = data; const od_id = _.get(data, 'order_detail'); const goods_id = _.get(data, 'goods._id'); const moneyDetail = await this.computedGoodsForRefund({ order_detail: od_id, goods_id }); if (moneyDetail) { const { payTotal } = moneyDetail; if (this.ctx.minus(payTotal, money) === 0) { // 添加到已退款的列表中 return true; } } } /** * 计算商品退货的金额最大值 * @param {Object} body 参数体 * @param body.order_detail 订单详情id * @param body.goods_id 商品id/套装id */ async computedGoodsForRefund({ order_detail, goods_id }) { const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); const moneyDetail = this.ctx.service.util.orderDetail.moneyDetail(orderDetail); const gmd = _.get(moneyDetail, goods_id); const obj = {}; obj.payTotal = _.get(gmd, 'grp'); obj.goodsTotal = _.get(gmd, 'st'); obj.freightTotal = _.get(gmd, 'ft'); obj.discountTotal = _.get(gmd, 'dt'); return obj; // const goodsTotal = // const goods = orderDetail.goods.find(f => f._id === goods_id); // // 货物支付金额, 数量*购买数量+ 数量*运费 - 优惠 // const goodsTotal = this.ctx.multiply(goods.sell_money, goods.buy_num); // const freightTotal = this.ctx.multiply(goods.freight, goods.buy_num); // let discountTotal = 0; // const { total_detail = {} } = orderDetail; // const { discount_detail = {} } = total_detail; // for (const dd in discount_detail) { // const dm = _.get(discount_detail, `${dd}.${goods_id}`, 0); // discountTotal = this.ctx.plus(discountTotal, dm); // } // const payTotal = this.ctx.minus(this.ctx.plus(goodsTotal, freightTotal), discountTotal); // const obj = { payTotal, goodsTotal, freightTotal, discountTotal }; // return obj; } /** * 退单 * @param {Object} body 参数体 * @param body.order_detail 订单详情id * @param body.desc 退单理由 */ async orderCancel({ order_detail, desc }) { // 查询要退的订单 const orderDetail = await this.orderDetailModel.findById(order_detail); if (!orderDetail) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); const { customer } = orderDetail; const basic = { order_detail, customer, type: '1', desc }; const moneyDetail = this.ctx.service.util.orderDetail.moneyDetail(orderDetail); let priceKey; if (_.get(orderDetail, 'type') === '1') priceKey = 'ggrp'; else priceKey = 'grp'; for (const goods_id in moneyDetail) { const d = _.get(moneyDetail, goods_id, {}); const money = _.get(d, priceKey, 0); const obj = { ...basic, goods_id, money }; await this.create(obj); } } async fetch(filter) { assert(filter); filter = await this.beforeFetch(filter); const { _id, id } = filter; if (_id || id) filter = { _id: ObjectId(_id || id) }; const { populate } = this.getRefMods(); let res = await this.model.findOne(filter).populate(populate).exec(); res = await this.afterFetch(filter, res); return res; } /** * 查询售后快递 * @param {Object} query 查询参数 * @param {String} query.id 售后id */ async getTransportInfo({ id }) { const data = await this.model.findById(id); if (!data) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到售后数据'); const { transport } = data; if (!transport) return; const { customer_transport_no, customer_transport_type, shop_transport_no, shop_transport_type } = transport; const result = {}; if (customer_transport_no && customer_transport_type) { const q = { no: customer_transport_no, type: customer_transport_type }; const customer = await this.ctx.service.util.kd100.search(q); result.customer = customer; } if (shop_transport_no && shop_transport_type) { const q = { no: shop_transport_no, type: shop_transport_type }; const shop = await this.ctx.service.util.kd100.search(q); result.shop = shop; } return result; } async canRefund({ id }) { const od = await this.orderDetailModel.findById(id, { buy_time: 1 }).lean(); if (!od) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单数据'); const config = await this.configModel.findOne({}, { reward_day: 1 }).lean(); const rd = _.get(config, 'reward_day', 14); const min = this.ctx.multiply(rd, this.ctx.multiply(24, 60)); const buy_time = _.get(od, 'buy_time'); const m = moment().diff(buy_time, 'm'); const r = m < min; return r; } } module.exports = AfterSaleService;