|
@@ -0,0 +1,433 @@
|
|
|
+'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 moment = require('moment');
|
|
|
+const { ObjectId } = require('mongoose').Types;
|
|
|
+const Transaction = require('mongoose-transactions');
|
|
|
+
|
|
|
+//
|
|
|
+class UserCouponService extends CrudService {
|
|
|
+ constructor(ctx) {
|
|
|
+ super(ctx, 'usercoupon');
|
|
|
+ this.model = this.ctx.model.User.UserCoupon;
|
|
|
+ this.couponModel = this.ctx.model.Trade.Coupon;
|
|
|
+ this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
|
|
|
+ this.tran = new Transaction();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改优惠券状态
|
|
|
+ * @param {Array} ids 用户领取的优惠券id列表
|
|
|
+ * @param {Transaction} tran 数据库事务实例
|
|
|
+ */
|
|
|
+ async useCoupon(ids, tran) {
|
|
|
+ for (const id of ids) {
|
|
|
+ tran.update('UserCoupon', id, { status: '1' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 领取优惠券
|
|
|
+ * @param {Object} params 地址参数
|
|
|
+ * @param params.coupon_id 优惠券id
|
|
|
+ */
|
|
|
+ async getCoupon({ coupon_id }) {
|
|
|
+ const coupon = await this.couponModel.findById(coupon_id);
|
|
|
+ if (!coupon) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到优惠券');
|
|
|
+ const canGet = parseInt(coupon.num) - 1 < 0;
|
|
|
+ if (canGet) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '您手慢了,没抢到');
|
|
|
+ const res = await this.checkCouponCanGet(coupon);
|
|
|
+ if (!res.result) return res;
|
|
|
+ const customer = _.get(this.ctx, 'user._id');
|
|
|
+ if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
|
|
|
+ const data = { coupon: coupon_id, customer };
|
|
|
+ try {
|
|
|
+ // 领券=>减券库存
|
|
|
+ this.tran.insert('UserCoupon', data);
|
|
|
+ this.tran.update('Coupon', coupon_id, { num: parseInt(coupon.num) - 1 });
|
|
|
+ await this.tran.run();
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error);
|
|
|
+ await this.tran.rollback();
|
|
|
+ throw new BusinessError(ErrorCode.SERVICE_FAULT, '领取优惠券失败');
|
|
|
+ } finally {
|
|
|
+ this.tran.clean();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查优惠券是否满足可领取的条件
|
|
|
+ * 可使用,就可以领取;否则只是垃圾数据,领了也用不了
|
|
|
+ * @param {Object} coupon 优惠券
|
|
|
+ */
|
|
|
+ async checkCouponCanGet(coupon) {
|
|
|
+ const { expire_type, expire_config, get_limit_config, get_limit, _id } = coupon;
|
|
|
+ // 1.检查优惠券是否生效: expire
|
|
|
+ const tr = this.checkCanGet_expire(expire_type, expire_config);
|
|
|
+ if (!tr.result) return tr;
|
|
|
+ // 2.不需要检查减免配置和使用配置,这两个配置是在使用的时候才检查
|
|
|
+ // 检查领取配置
|
|
|
+ const gr = this.checkCanGet_get(get_limit, get_limit_config, _id);
|
|
|
+ if (!gr.result) return gr;
|
|
|
+ return { result: true };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查优惠券的过期时间是否符合领取条件
|
|
|
+ * 是否过了 失效时间
|
|
|
+ * 只有时间段的情况才会有这个问题.领取后开始计算失效问题不需要检查
|
|
|
+ * @param {String} type 过期类型
|
|
|
+ * @param {Object} config 过期设置
|
|
|
+ */
|
|
|
+ checkCanGet_expire(type, config) {
|
|
|
+ const res = { result: true };
|
|
|
+ if (type === 'fixed') {
|
|
|
+ const end = _.last(_.get(config, 'fixed'));
|
|
|
+ if (!end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
|
|
|
+ const r = moment().isBefore(end);
|
|
|
+ if (!r) {
|
|
|
+ res.result = false;
|
|
|
+ res.msg = '已超过优惠券可使用日期';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 检查优惠券的领取设置是否符合领取条件
|
|
|
+ * nolimit直接放过,max需要查看confing里面的设置
|
|
|
+ * @param {String} type 领取类型
|
|
|
+ * @param {Object} config 领取设置
|
|
|
+ * @param {String} coupon 优惠券id
|
|
|
+ */
|
|
|
+ async checkCanGet_get(type, config, coupon) {
|
|
|
+ const res = { result: true };
|
|
|
+ if (type === 'nolimit') return res;
|
|
|
+ const max = _.get(config, type);
|
|
|
+ // 需要找到用户
|
|
|
+ const customer = _.get(this.ctx, 'user._id');
|
|
|
+ if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
|
|
|
+ const num = await this.model.count({ coupon, user: customer });
|
|
|
+ if (num >= max) {
|
|
|
+ res.result = false;
|
|
|
+ res.msg = '该优惠券已到达领取上限';
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据用户和下单页的商品,对用户的优惠券进行检查,整理数据
|
|
|
+ * @param {Array} goods 商品列表
|
|
|
+ */
|
|
|
+ async toMakeOrder_getList(goods) {
|
|
|
+ const customer = _.get(this.ctx, 'user._id');
|
|
|
+ if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
|
|
|
+ const { populate } = this.getRefMods();
|
|
|
+ let couponList = await this.model.find({ customer, status: '0' }, { customer: 0 }).populate(populate);
|
|
|
+ couponList = JSON.parse(JSON.stringify(couponList));
|
|
|
+ // 接下来检查哪些优惠券符合条件
|
|
|
+ for (const uc of couponList) {
|
|
|
+ // 1.检查失效问题
|
|
|
+ let r = this.checkCanUse_expire(uc);
|
|
|
+ if (!r) {
|
|
|
+ uc.canUse = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 2.检查使用问题
|
|
|
+ r = this.checkCanUse_use(uc, goods);
|
|
|
+ if (!r) {
|
|
|
+ uc.canUse = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ uc.canUse = true;
|
|
|
+ }
|
|
|
+ return couponList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查优惠券的过期时间是否符合使用条件
|
|
|
+ * @param {Object} userCoupon 用户领取的优惠券
|
|
|
+ */
|
|
|
+ checkCanUse_expire(userCoupon) {
|
|
|
+ const { coupon, meta } = userCoupon;
|
|
|
+ const { expire_config, expire_type } = coupon;
|
|
|
+ if (expire_type === 'fixed') {
|
|
|
+ // 在设置的时间段内,则允许使用
|
|
|
+ const start = _.head(_.get(expire_config, 'fixed'));
|
|
|
+ const end = _.last(_.get(expire_config, 'fixed'));
|
|
|
+ if (!start || !end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
|
|
|
+ return moment().isBetween(start, end, null, '[]');
|
|
|
+ } else if (expire_type === 'days') {
|
|
|
+ let getDate = _.get(meta, 'createdAt');
|
|
|
+ if (!getDate) return false;
|
|
|
+ // 计算出过期时间, 再看下当前日期是否超过该日期
|
|
|
+ getDate = moment(getDate).format('YYYY-MM-DD HH:mm:ss');
|
|
|
+ const days = _.get(expire_config, 'days');
|
|
|
+ const expire_date = moment(getDate).add(days, 'days');
|
|
|
+ return moment().isSameOrBefore(expire_date);
|
|
|
+ } else if (expire_type === 'nolimit') return true; // 没有过期日期;可以使用
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查该优惠券是否可以在这些商品中使用
|
|
|
+ * @param {Object} userCoupon 用户领取的优惠券
|
|
|
+ * @param {Array} shopGoods 按店铺分组的商品列表
|
|
|
+ */
|
|
|
+ checkCanUse_use(userCoupon, shopGoods) {
|
|
|
+ const { coupon } = userCoupon;
|
|
|
+ const { use_limit, use_limit_config = {}, shop } = coupon;
|
|
|
+ const { tags = [] } = use_limit_config;
|
|
|
+ // 全类型商品,不需要继续检查
|
|
|
+ if (use_limit === 'all' || tags.length === 0) return true;
|
|
|
+ // 检查的所有商品中,只要有一个可以使用,就放过
|
|
|
+ let result = false;
|
|
|
+ for (const s of shopGoods) {
|
|
|
+ // 检查优惠券是否是店铺发行,如果是店铺发行;
|
|
|
+ // 则需要在指定店铺使用,如果不是,则直接检查下一个
|
|
|
+ if (shop && shop !== s.shop) continue;
|
|
|
+ // 是这个店铺的优惠券,则需要检查这个店铺下的商品是否能用这个优惠券
|
|
|
+ // 目前只有商品分类(tags标签)来进行设置
|
|
|
+ // 商品列表(goods)中只要有 标签(tags) 中能找到 和 tags:
|
|
|
+ /** e.g.:
|
|
|
+ const arr = [
|
|
|
+ ['xxsp', 'jgch', 'htr'],
|
|
|
+ ['xxsp', 'jgch', 'ht'],
|
|
|
+ ];
|
|
|
+ const arr1 = [
|
|
|
+ ['xxsp', 'jgch', 'ht1r'],
|
|
|
+ ['xxsp', 'jgch', 'ht'],
|
|
|
+ ];
|
|
|
+ const r = arr.findIndex((f) => arr1.find((ff) => _.isEqual(f, ff)));
|
|
|
+ console.log(r);
|
|
|
+ */
|
|
|
+ const r = s.goods.some((g) => g.tags.find((f) => tags.find((ff) => _.isEqual(f, ff))));
|
|
|
+ if (r) {
|
|
|
+ result = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算订单使用优惠券的明细
|
|
|
+ * @param {Array} couponIds 订单使用的用户领取的优惠券数据id数组
|
|
|
+ * @param {Array} goodsSpecs 订单所有商品规格的数据:{id,buy_num}
|
|
|
+ */
|
|
|
+ async computedOrderCouponDiscount(couponIds, goodsSpecs) {
|
|
|
+ const result = {}; // 最终结果: 以 couponId: { goodsSpecId: ${money} } 为明细格式
|
|
|
+ const goodsSpecIds = goodsSpecs.map((i) => i.id);
|
|
|
+ const { populate: gp } = this.ctx.service.shop.goodsSpec.getRefMods();
|
|
|
+ let goodsSpecList = await this.goodsSpecModel.find({ _id: goodsSpecIds }).populate(gp);
|
|
|
+ goodsSpecList = this.resetGoodsSpecData(goodsSpecList, goodsSpecs);
|
|
|
+ const { populate: cp } = this.getRefMods();
|
|
|
+ let userCouponList = await this.model.find({ _id: couponIds }, { customer: 0 }).populate(cp);
|
|
|
+ // 先查下有没有不能用的,有不能用的需要返回
|
|
|
+ let cantUse = userCouponList.find((f) => f.status !== '0');
|
|
|
+ if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}已使用`);
|
|
|
+ cantUse = userCouponList.find((f) => !this.checkCanUse_expire(f));
|
|
|
+ if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}不能使用`);
|
|
|
+ userCouponList = this.resetUserCouponData(userCouponList);
|
|
|
+ // 上面整理完数据之后,下面开始用 均分优惠金额
|
|
|
+ for (const coupon of userCouponList) {
|
|
|
+ // 可以被优惠的商品列表
|
|
|
+ let goodsList = [];
|
|
|
+ // 1.先找到满足优惠条件的商品
|
|
|
+ const { issue, shop, use_limit, use_limit_config, _id } = coupon;
|
|
|
+ // 平台券,都可以
|
|
|
+ if (issue === '0') goodsList = goodsSpecList;
|
|
|
+ // 店铺券,需要过滤店铺
|
|
|
+ else goodsList = goodsSpecList.filter((f) => ObjectId(shop).equals(_.get(f, 'shop._id')));
|
|
|
+ // 2.商品已确定,再确定是否是指定分类
|
|
|
+ if (use_limit !== 'all') {
|
|
|
+ // all的情况不需要再次过滤出不符合的条件: all为全部商品类型
|
|
|
+ const tags = _.get(use_limit_config, 'tags', []);
|
|
|
+ goodsList = this.getGoodsInTags(goodsSpecList, tags);
|
|
|
+ }
|
|
|
+ // 3.开始分配
|
|
|
+ const discount_detail = this.divideByCoupon(coupon, goodsList);
|
|
|
+ result[_id] = discount_detail;
|
|
|
+ }
|
|
|
+ // console.log(result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分配减免
|
|
|
+ * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
|
|
|
+ * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
|
|
|
+ */
|
|
|
+ divideByCoupon(coupon, goodsList) {
|
|
|
+ const { discount_type, discount_config } = coupon;
|
|
|
+ const { limit, min, max } = discount_config;
|
|
|
+ // 符合条件的商品的总价
|
|
|
+ const goodsTotal = goodsList.reduce((p, n) => p + (parseFloat(n.goods_total) || 0), 0);
|
|
|
+ if (limit !== 'nolimit') {
|
|
|
+ // 有最低消费的限制,就需要看看下满足条件的商品够不够这个金额
|
|
|
+ // 消费下限 大于 满足优惠券使用的商品的消费总额,则不能使用这个券,消费的钱不够
|
|
|
+ // 且这个地方应该报错
|
|
|
+ if (parseFloat(limit) > goodsTotal) throw new BusinessError(ErrorCode.DATA_INVALID, `消费券:${coupon.name}不满足使用条件`);
|
|
|
+ }
|
|
|
+ // 减免设置转换成 实际减免总金额:
|
|
|
+ // 满减则直接就是 min(设置的满x减y的y);
|
|
|
+ // 折扣的话,需要计算,是否超过折扣上限(max):
|
|
|
+ // 超过上限则 为上限设置的金额,和满减一样按比例分配即可;
|
|
|
+ // 没超过上限,则为各自去打折即可
|
|
|
+ let discountRealMoney = 0;
|
|
|
+ let outLimit = true; // 是否超过金额上限
|
|
|
+ //
|
|
|
+ if (discount_type === 'min') discountRealMoney = parseFloat(min);
|
|
|
+ else {
|
|
|
+ // 需要计算是否超过上限
|
|
|
+ // 没有上限,那就不会超过
|
|
|
+ if (!max) outLimit = false;
|
|
|
+ else {
|
|
|
+ // 有上限,则需要计算
|
|
|
+ const percent = 1 - parseFloat(min) / 10;
|
|
|
+ // 计算实际折扣 减了 多少钱
|
|
|
+ const discountMoney = _.floor(goodsTotal * percent, 2);
|
|
|
+ // 最后看下 不限制减的钱 是不是 超过了 上限
|
|
|
+ outLimit = discountMoney > parseFloat(max);
|
|
|
+ // 超过了,就将上限的金额拿去按比例分配
|
|
|
+ if (outLimit) discountRealMoney = parseFloat(max);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据是否超过上限金额来决定策略:
|
|
|
+ // 超过上限金额, 按 每种货物的总价值 占 这些货物应该交的钱 的比例进行优惠分配
|
|
|
+ // 没超过上限(目前:只有折扣会有这个情况), 则每个货物都按比例来计算
|
|
|
+ if (!outLimit) return this.discountPercentForEachGoods(coupon, goodsList);
|
|
|
+ return this.divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按比例计算每件商品,这里就不需要优惠券什么事了.需要优惠券的东西参数都带来了
|
|
|
+ * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
|
|
|
+ * @param {Number} discountRealMoney 需要按比例分配的优惠金额
|
|
|
+ * @param {Number} goodsTotal goodsList的商品总金额
|
|
|
+ */
|
|
|
+ divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal) {
|
|
|
+ const result = {};
|
|
|
+ // 已分配优惠的金额的总数
|
|
|
+ let allReadyDiscount = 0;
|
|
|
+ for (let i = 0; i < goodsList.length; i++) {
|
|
|
+ const g = goodsList[i];
|
|
|
+ const { goods_total } = g;
|
|
|
+ const id = _.get(g, 'goodsSpec._id');
|
|
|
+ const obj = { original: goods_total };
|
|
|
+ // 除了最后一个商品,其余的商品均按比例分配
|
|
|
+ if (i < goodsList.length - 1) {
|
|
|
+ const percent = _.floor(goods_total / goodsTotal);
|
|
|
+ const discountMoney = _.floor(discountRealMoney * percent, 2);
|
|
|
+ const realPay = _.floor(goods_total - discountMoney, 2);
|
|
|
+ obj.realPay = realPay;
|
|
|
+ obj.discountMoney = discountMoney;
|
|
|
+ allReadyDiscount += discountMoney;
|
|
|
+ } else {
|
|
|
+ // 最后一个商品是剩余的可分配的钱
|
|
|
+ const discountMoney = _.floor(discountRealMoney - allReadyDiscount, 2);
|
|
|
+ const realPay = _.floor(goods_total - discountMoney, 2);
|
|
|
+ obj.realPay = realPay;
|
|
|
+ obj.discountMoney = discountMoney;
|
|
|
+ }
|
|
|
+ result[id] = obj;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按折扣计算每件商品
|
|
|
+ * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
|
|
|
+ * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
|
|
|
+ */
|
|
|
+ discountPercentForEachGoods(coupon, goodsList) {
|
|
|
+ const { discount_config } = coupon;
|
|
|
+ const { min } = discount_config;
|
|
|
+ const percent = parseFloat(min) / 10;
|
|
|
+ const result = {};
|
|
|
+ for (const g of goodsList) {
|
|
|
+ const { goods_total } = g;
|
|
|
+ const id = _.get(g, 'goodsSpec._id');
|
|
|
+ const realPay = _.floor(goods_total * percent, 2);
|
|
|
+ const discountMoney = goods_total - realPay;
|
|
|
+ const obj = {
|
|
|
+ original: goods_total,
|
|
|
+ realPay,
|
|
|
+ discountMoney,
|
|
|
+ };
|
|
|
+ result[id] = obj;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 过滤出 订单中商品规格数据 在这些标签中的 商品规格
|
|
|
+ * @param {Array} goodsSpecList 订单中商品规格列表
|
|
|
+ * @param {Array} tags 商品标签
|
|
|
+ */
|
|
|
+ getGoodsInTags(goodsSpecList, tags) {
|
|
|
+ const arr = goodsSpecList.filter((f) => {
|
|
|
+ const gtags = _.get(f, 'goods.tags');
|
|
|
+ // 没打标签或者标签里没有内容的直接过滤出去
|
|
|
+ if (!gtags || gtags.length <= 0) return false;
|
|
|
+ const r = gtags.find((f) => tags.find((ff) => _.isEqual(ff, f)));
|
|
|
+ return r;
|
|
|
+ });
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 整理商品规格列表数据
|
|
|
+ * 将goods,shop和goodsSpec重组为3个属性在object同一级上
|
|
|
+ * @param {Array} list 商品规格列表
|
|
|
+ * @param {Array} orderInfo 有关订单的信息 {id,buy_num}
|
|
|
+ */
|
|
|
+ resetGoodsSpecData(list, orderInfo) {
|
|
|
+ const nouse = ['meta', '__v'];
|
|
|
+ const arr = list.map((i) => {
|
|
|
+ const obj = {};
|
|
|
+ const goodsSpec = _.omit(i, [...nouse, 'goods']);
|
|
|
+ const shop = _.omit(_.get(i, 'goods.shop'), [...nouse]);
|
|
|
+ const goods = _.omit(_.get(i, 'goods'), [...nouse, 'shop']);
|
|
|
+ obj.goodsSpec = goodsSpec;
|
|
|
+ obj.shop = shop;
|
|
|
+ obj.goods = goods;
|
|
|
+ const r = orderInfo.find((f) => ObjectId(f.id).equals(goodsSpec._id));
|
|
|
+ if (r) obj.buy_num = r.buy_num;
|
|
|
+ if (obj.buy_num) {
|
|
|
+ obj.goods_total = parseInt(obj.buy_num) * parseFloat(_.get(obj.goodsSpec, 'sell_money'));
|
|
|
+ }
|
|
|
+ return obj;
|
|
|
+ });
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 整理出下面所需要使用的数据,并将这些数据放在object的第一级上
|
|
|
+ * @param {Array} list 用户优惠券列表
|
|
|
+ */
|
|
|
+ resetUserCouponData(list) {
|
|
|
+ const arr = list.map((i) => {
|
|
|
+ const { _id, meta, coupon } = i;
|
|
|
+ const cd = _.pick(coupon, [
|
|
|
+ 'issue',
|
|
|
+ 'shop',
|
|
|
+ 'expire_type',
|
|
|
+ 'expire_config',
|
|
|
+ 'discount_type',
|
|
|
+ 'discount_config',
|
|
|
+ 'use_limit',
|
|
|
+ 'use_limit_config',
|
|
|
+ 'get_limit',
|
|
|
+ 'get_limit_config',
|
|
|
+ 'name',
|
|
|
+ ]);
|
|
|
+ const obj = { _id, meta, ...cd };
|
|
|
+ return obj;
|
|
|
+ });
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = UserCouponService;
|