forked from wangziqi/gongxue-base
feat: add coupon rule reporting
This commit is contained in:
@@ -2150,6 +2150,175 @@ async function testCommerce() {
|
||||
});
|
||||
assert.equal(freePaymentDenied.code, 'ORDER_ALREADY_PAID', 'paid zero-amount order should not create another payment');
|
||||
|
||||
const minOrderCoupon = await request('/api/tenant-admin/coupons', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: `ITMIN${Date.now()}`,
|
||||
planId: ids.couponOnlyPlan,
|
||||
discountType: 'fixed',
|
||||
discountValue: 300,
|
||||
minOrderAmountCents: 1500,
|
||||
campaignName: 'integration-coupon-rules',
|
||||
maxUses: 50,
|
||||
},
|
||||
});
|
||||
assert.equal(minOrderCoupon.item?.minOrderAmountCents, 1500, 'tenant admin should persist coupon minimum amount rule');
|
||||
const minOrderCouponDenied = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: minOrderCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(minOrderCouponDenied.code, 'COUPON_MIN_ORDER_AMOUNT_NOT_MET', 'coupon minimum order amount should be enforced by backend');
|
||||
|
||||
const disabledCoupon = await request('/api/tenant-admin/coupons', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: `ITDIS${Date.now()}`,
|
||||
planId: ids.couponOnlyPlan,
|
||||
discountType: 'fixed',
|
||||
discountValue: 100,
|
||||
status: 'disabled',
|
||||
campaignName: 'integration-coupon-rules',
|
||||
},
|
||||
});
|
||||
const disabledCouponDenied = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: disabledCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(disabledCouponDenied.code, 'COUPON_DISABLED', 'disabled coupon should not be claimable');
|
||||
|
||||
const regionRestrictedCoupon = await request('/api/tenant-admin/coupons', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: `ITREG${Date.now()}`,
|
||||
planId: ids.couponOnlyPlan,
|
||||
discountType: 'fixed',
|
||||
discountValue: 100,
|
||||
allowedRegionIds: ['00000000-0000-0000-0000-000000000999'],
|
||||
campaignName: 'integration-coupon-rules',
|
||||
},
|
||||
});
|
||||
const regionCouponDenied = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: regionRestrictedCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(regionCouponDenied.code, 'COUPON_REGION_MISMATCH', 'coupon allowed region rule should be enforced');
|
||||
|
||||
const multiUseCoupon = await request('/api/tenant-admin/coupons', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: `ITMULTI${Date.now()}`,
|
||||
planId: ids.couponOnlyPlan,
|
||||
discountType: 'fixed',
|
||||
discountValue: 100,
|
||||
maxDiscountCents: 80,
|
||||
perUserLimit: 2,
|
||||
allowedPlanIds: [ids.couponOnlyPlan],
|
||||
allowedRegionIds: [ids.region],
|
||||
campaignName: 'integration-coupon-rules',
|
||||
metadata: { scenario: 'multi-use' },
|
||||
},
|
||||
});
|
||||
assert.equal(multiUseCoupon.item?.maxDiscountCents, 80, 'tenant admin should persist max discount cap');
|
||||
assert.equal(multiUseCoupon.item?.perUserLimit, 2, 'tenant admin should persist per-user limit');
|
||||
|
||||
const multiClaimOne = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: multiUseCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
});
|
||||
assert.equal(multiClaimOne.coupon?.discountCents, 80, 'coupon max discount cap should apply during claim');
|
||||
const multiOrderOne = await request('/api/commerce/orders', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
planId: ids.couponOnlyPlan,
|
||||
payProvider: 'manual',
|
||||
payMethod: 'manual',
|
||||
regionId: ids.region,
|
||||
couponRedemptionId: multiClaimOne.redemption.id,
|
||||
},
|
||||
});
|
||||
assert.equal(multiOrderOne.item?.discountCents, 80, 'order should apply capped coupon discount');
|
||||
|
||||
const multiClaimTwo = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: multiUseCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
});
|
||||
assert.notEqual(multiClaimTwo.redemption?.id, multiClaimOne.redemption?.id, 'perUserLimit > 1 should allow another redemption after first use');
|
||||
const multiOrderTwo = await request('/api/commerce/orders', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
planId: ids.couponOnlyPlan,
|
||||
payProvider: 'manual',
|
||||
payMethod: 'manual',
|
||||
regionId: ids.region,
|
||||
couponRedemptionId: multiClaimTwo.redemption.id,
|
||||
},
|
||||
});
|
||||
assert.equal(multiOrderTwo.item?.discountCents, 80, 'second allowed coupon use should still apply cap');
|
||||
|
||||
const multiClaimThreeDenied = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: multiUseCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(multiClaimThreeDenied.code, 'COUPON_USER_LIMIT_REACHED', 'per-user coupon limit should be enforced after allowed uses');
|
||||
|
||||
const firstOrderCoupon = await request('/api/tenant-admin/coupons', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: `ITFIRST${Date.now()}`,
|
||||
planId: ids.couponOnlyPlan,
|
||||
discountType: 'fixed',
|
||||
discountValue: 100,
|
||||
firstOrderOnly: true,
|
||||
campaignName: 'integration-coupon-rules',
|
||||
},
|
||||
});
|
||||
const firstOrderDenied = await request('/api/commerce/coupons/claim', {
|
||||
method: 'POST',
|
||||
body: { code: firstOrderCoupon.item.code, planId: ids.couponOnlyPlan, regionId: ids.region },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(firstOrderDenied.code, 'COUPON_FIRST_ORDER_ONLY', 'first-order-only coupon should reject users with paid orders');
|
||||
|
||||
const couponReport = await request('/api/tenant-admin/coupons/report', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { campaignName: 'integration-coupon-rules' },
|
||||
});
|
||||
assert.ok(couponReport.item?.claimCount >= 2, 'coupon report should aggregate campaign claims');
|
||||
assert.ok(couponReport.item?.usedCount >= 2, 'coupon report should aggregate used redemptions');
|
||||
assert.ok(
|
||||
couponReport.item?.byCoupon?.some(item => item.id === multiUseCoupon.item.id && item.redeemedCount >= 2),
|
||||
'coupon report should include per-coupon redemption stats',
|
||||
);
|
||||
|
||||
const couponRedemptions = await request('/api/tenant-admin/coupons/redemptions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { couponId: multiUseCoupon.item.id, status: 'used' },
|
||||
});
|
||||
assert.ok(
|
||||
couponRedemptions.items?.some(item => item.orderNo === multiOrderOne.item.orderNo),
|
||||
'coupon redemption list should include used order identity',
|
||||
);
|
||||
|
||||
const couponRedemptionsOperatorDenied = await request('/api/tenant-admin/coupons/redemptions', {
|
||||
userId: TENANT_OPERATOR_USER_ID,
|
||||
query: { couponId: multiUseCoupon.item.id, status: 'used' },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(
|
||||
couponRedemptionsOperatorDenied.code,
|
||||
'TENANT_PERMISSION_REQUIRED',
|
||||
'coupon redemption list should require dedicated redemption-report permission beyond coupon config read',
|
||||
);
|
||||
|
||||
const partialRefundOrder = await request('/api/commerce/orders', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -6356,10 +6525,20 @@ async function testTenantAdminOps() {
|
||||
discountType: 'fixed',
|
||||
discountValue: 10,
|
||||
maxUses: 100,
|
||||
minOrderAmountCents: 100,
|
||||
maxDiscountCents: 10,
|
||||
perUserLimit: 2,
|
||||
allowedPlanIds: ['00000000-0000-0000-0000-000000000201'],
|
||||
allowedRegionIds: [ids.region],
|
||||
campaignName: 'tenant-admin-coupon-smoke',
|
||||
metadata: { channel: 'integration' },
|
||||
source: 'integration-test',
|
||||
},
|
||||
});
|
||||
assert.equal(coupon.item?.code, 'IT-COUPON-001', 'tenant admin should upsert coupon');
|
||||
assert.equal(coupon.item?.campaignName, 'tenant-admin-coupon-smoke', 'tenant admin should persist coupon campaign');
|
||||
assert.equal(coupon.item?.perUserLimit, 2, 'tenant admin should persist coupon per-user limit');
|
||||
assert.ok(coupon.item?.allowedRegionIds?.includes(ids.region), 'tenant admin should persist coupon region scope');
|
||||
|
||||
const partnerDenied = await request('/api/tenant-admin/auth-providers', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
@@ -6530,7 +6709,8 @@ async function testTenantMemberPermissionsAndAudit() {
|
||||
status: 'active',
|
||||
permissions: {
|
||||
'codes:*': true,
|
||||
'coupons:*': true,
|
||||
'coupons:read': true,
|
||||
'coupons:write': true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -7031,7 +7211,8 @@ async function testReferralAndCrmGrowth() {
|
||||
status: 'active',
|
||||
permissions: {
|
||||
'codes:*': true,
|
||||
'coupons:*': true,
|
||||
'coupons:read': true,
|
||||
'coupons:write': true,
|
||||
'referral:*': true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user