fix: disable starter pack server purchase guard
This commit is contained in:
parent
b8e02e08d0
commit
038743f3f2
|
|
@ -185,21 +185,10 @@ export function starterPackRound(user: any): number {
|
|||
: Number(user?.starter_pack) > 0 || Number(user?.starter_packState) === 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
export function starterPackOrderSnapshot(user: any, itemid: string) {
|
||||
export function starterPackOrderMetadata(itemid: string) {
|
||||
if (!/^starter_pack(?:_[2-5])?$/.test(itemid)) return {};
|
||||
if (!user || !Number.isFinite(Number(user.levelAmount)) || Number(user.levelAmount) < 15
|
||||
|| Number(user.starter_packState) === 1 || !(Number(user.starter_pack) > Date.now())) {
|
||||
throw new Error('本轮礼包未开放或已购买');
|
||||
}
|
||||
const offer = user.starterPackOffer ?? starterPackPatch(user, Date.now())?.starterPackOffer;
|
||||
if (!offer || offer.productId !== itemid || offer.expiresAt !== Number(user.starter_pack)) {
|
||||
throw new Error('礼包档位已更新,请重新打开礼包');
|
||||
}
|
||||
return {
|
||||
starterPackOffer: offer,
|
||||
starterPackBuyRound: Math.max(1, starterPackRound(user)),
|
||||
starterPackExpiresAt: Math.max(0, Number(user?.starter_pack) || 0),
|
||||
rewardVersion: offer.rewardVersion,
|
||||
rewardVersion: STARTER_REWARD_VERSION,
|
||||
activityType: 'lucky',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,16 +484,7 @@ async function getIosOrder(openid, ctx) {
|
|||
case "starter_pack_3":
|
||||
case "starter_pack_4":
|
||||
case "starter_pack_5": {
|
||||
const order = res.data[i], user = res1.data;
|
||||
const legacyCycle = (!user.starterPackOffer || user.starterPackOffer.legacy === true) && Number(order.time) >= Number(user.starter_pack) - 2 * 86400000
|
||||
&& Number(order.time) <= Number(user.starter_pack);
|
||||
const expiry = order.starterPackExpiresAt ?? (legacyCycle ? user.starter_pack : null);
|
||||
if (expiry != null && Number(expiry) === Number(user.starter_pack)
|
||||
&& (order.starterPackBuyRound == null || Number(order.starterPackBuyRound) === (Number(user.starterPackRound) || 1))) {
|
||||
await db.collection("users").where({ _id: user._id, starter_pack: user.starter_pack,
|
||||
starterPackRound: user.starterPackRound ?? null }).update({ starter_packState: 1 });
|
||||
}
|
||||
await db.collection("users").where({ _id: user._id }).update({ pay_user: true });
|
||||
await db.collection("users").where({ _id: res1.data._id }).update({ starter_packState: 1, pay_user: true });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -186,13 +186,11 @@ async function PostImg(OpenId, outTradeNo, name, count, price, passInput: any =
|
|||
if (!token) {
|
||||
return { code: 0, data: null, msg: "token获取失败" };
|
||||
}
|
||||
const giftUser = /^starter_pack(?:_[2-5])?$/.test(name) ? (await db.collection("users").where({ openid: OpenId }).getOne()).data : null;
|
||||
let giftSnapshot: any = {};
|
||||
try {
|
||||
if (String(name).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(name)) throw new Error("无效的礼包商品");
|
||||
giftSnapshot = StarterPack.starterPackOrderSnapshot(giftUser, name);
|
||||
if (giftSnapshot.starterPackOffer) { count = 1; price = giftSnapshot.starterPackOffer.priceFen; }
|
||||
} catch (error) { await PostMsg(OpenId, error.message); return; }
|
||||
if (String(name).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(name)) {
|
||||
await PostMsg(OpenId, "无效的礼包商品");
|
||||
return;
|
||||
}
|
||||
const giftMetadata = StarterPack.starterPackOrderMetadata(name);
|
||||
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`;
|
||||
const nonceStr = crypto.randomBytes(16).toString('hex');
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
|
|
@ -226,7 +224,7 @@ async function PostImg(OpenId, outTradeNo, name, count, price, passInput: any =
|
|||
}
|
||||
await db.collection("iosOrder").add({
|
||||
...passFields,
|
||||
...giftSnapshot,
|
||||
...giftMetadata,
|
||||
openid: OpenId,
|
||||
outTradeNo: outTradeNo,
|
||||
itemid: name,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,8 @@ export default async function (ctx: FunctionContext) {
|
|||
openid: res.data.openid,
|
||||
outTradeNo: res.data.outTradeNo,
|
||||
itemid: res.data.itemid,
|
||||
...(res.data.starterPackBuyRound != null ? { starterPackBuyRound: res.data.starterPackBuyRound } : {}),
|
||||
...(res.data.starterPackExpiresAt != null ? { starterPackExpiresAt: res.data.starterPackExpiresAt } : {}),
|
||||
...(res.data.starterPackOffer ? {
|
||||
starterPackOffer: res.data.starterPackOffer,
|
||||
activityType: res.data.activityType, rewardVersion: res.data.rewardVersion,
|
||||
} : {}),
|
||||
...(res.data.activityType ? { activityType: res.data.activityType } : {}),
|
||||
...(res.data.rewardVersion ? { rewardVersion: res.data.rewardVersion } : {}),
|
||||
itemCount: res.data.itemCount,
|
||||
goodsPrice: res.data.goodsPrice,
|
||||
time: res.data.time,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export default async function (ctx: FunctionContext) {
|
|||
}
|
||||
}
|
||||
return { code: 1, data: "ok", msg: "成功", ...(/^starter_pack(?:_[2-5])?$/.test(order.data.itemid) ? {
|
||||
starterPackOffer: order.data.starterPackOffer, itemid: order.data.itemid,
|
||||
starterPackBuyRound: order.data.starterPackBuyRound,
|
||||
itemid: order.data.itemid,
|
||||
} : {}) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@ export default async function (ctx: FunctionContext) {
|
|||
if (res.data.openid !== user.data.openid) {
|
||||
return { code: 0, data: null, msg: "充值订单不属于当前玩家" };
|
||||
}
|
||||
const starterData = /^starter_pack(?:_[2-5])?$/.test(res.data.itemid) ? {
|
||||
starterPackOffer: res.data.starterPackOffer, itemid: res.data.itemid,
|
||||
starterPackBuyRound: res.data.starterPackBuyRound,
|
||||
} : {};
|
||||
const starterData = /^starter_pack(?:_[2-5])?$/.test(res.data.itemid) ? { itemid: res.data.itemid } : {};
|
||||
if (res.data.state == 1 || res.data.state == 2) {
|
||||
// Retry only events already captured by the payment callback; do not backfill
|
||||
// historical completed orders using today's remaining time.
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ export default async function (ctx: FunctionContext) {
|
|||
}
|
||||
let payment_name = ret.data?.itemid;
|
||||
const starterGift = /^starter_pack(?:_[2-5])?$/.test(payment_name);
|
||||
const starterData = starterGift ? { starterPackOffer: ret.data.starterPackOffer,
|
||||
itemid: payment_name, starterPackBuyRound: ret.data.starterPackBuyRound } : {};
|
||||
const starterData = starterGift ? { itemid: payment_name } : {};
|
||||
if (starterGift) {
|
||||
const owner = uid ? (await db.collection("users").where({ _id: uid }).getOne()).data : null;
|
||||
if (!owner || owner.openid !== ret.data.openid) return { code: 0, data: null, msg: "充值订单不属于当前玩家" };
|
||||
|
|
@ -95,15 +94,7 @@ export default async function (ctx: FunctionContext) {
|
|||
case "starter_pack_3":
|
||||
case "starter_pack_4":
|
||||
case "starter_pack_5": {
|
||||
const legacyCycle = (!res1.data.starterPackOffer || res1.data.starterPackOffer.legacy === true) && Number(ret.data.time) >= Number(res1.data.starter_pack) - 2 * 86400000
|
||||
&& Number(ret.data.time) <= Number(res1.data.starter_pack);
|
||||
const expiry = ret.data.starterPackExpiresAt ?? (legacyCycle ? res1.data.starter_pack : null);
|
||||
if (expiry != null && Number(expiry) === Number(res1.data.starter_pack)
|
||||
&& (ret.data.starterPackBuyRound == null || Number(ret.data.starterPackBuyRound) === (Number(res1.data.starterPackRound) || 1))) {
|
||||
await db.collection("users").where({ _id: uid, starter_pack: res1.data.starter_pack,
|
||||
starterPackRound: res1.data.starterPackRound ?? null }).update({ starter_packState: 1 });
|
||||
}
|
||||
await db.collection("users").where({ _id: uid }).update({ pay_user: true });
|
||||
await db.collection("users").where({ _id: uid }).update({ starter_packState: 1, pay_user: true });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -42,12 +42,10 @@ export default async function (ctx: FunctionContext) {
|
|||
if (user.data == null) {
|
||||
return { code: 0, data: null, msg: "未获取到玩家数据" };
|
||||
}
|
||||
let starterFields: any = {};
|
||||
try {
|
||||
if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) throw new Error("无效的礼包商品");
|
||||
starterFields = StarterPack.starterPackOrderSnapshot(user.data, itemid);
|
||||
if (starterFields.starterPackOffer) { itemCount = 1; itemPrice = starterFields.starterPackOffer.priceFen; }
|
||||
} catch (error) { return { code: 0, data: null, msg: error.message }; }
|
||||
if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) {
|
||||
return { code: 0, data: null, msg: "无效的礼包商品" };
|
||||
}
|
||||
const starterFields = StarterPack.starterPackOrderMetadata(itemid);
|
||||
const jungleTier = JungleConfig.getJunglePaidTierByProductId(itemid);
|
||||
let passFields: any = {};
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -42,12 +42,10 @@ export default async function (ctx: FunctionContext) {
|
|||
if (user.data == null) {
|
||||
return { code: 0, data: null, msg: "未获取到玩家数据" };
|
||||
}
|
||||
let starterFields: any = {};
|
||||
try {
|
||||
if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) throw new Error("无效的礼包商品");
|
||||
starterFields = StarterPack.starterPackOrderSnapshot(user.data, itemid);
|
||||
if (starterFields.starterPackOffer) { itemCount = 1; itemPrice = starterFields.starterPackOffer.priceFen; }
|
||||
} catch (error) { return { code: 0, data: null, msg: error.message }; }
|
||||
if (String(itemid).startsWith("starter_pack") && !/^starter_pack(?:_[2-5])?$/.test(itemid)) {
|
||||
return { code: 0, data: null, msg: "无效的礼包商品" };
|
||||
}
|
||||
const starterFields = StarterPack.starterPackOrderMetadata(itemid);
|
||||
const jungleTier = JungleConfig.getJunglePaidTierByProductId(itemid);
|
||||
let passFields: any = {};
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -112,15 +112,7 @@ export default async function (ctx: FunctionContext) {
|
|||
}
|
||||
|
||||
if (/^starter_pack(?:_[2-5])?$/.test(order.itemid)) {
|
||||
// An earlier round may finish paying after a newer offer has opened.
|
||||
const legacyCycle = (!user.starterPackOffer || user.starterPackOffer.legacy === true) && Number(order.time) >= Number(user.starter_pack) - 2 * 86400000
|
||||
&& Number(order.time) <= Number(user.starter_pack);
|
||||
const expiry = order.starterPackExpiresAt ?? (legacyCycle ? user.starter_pack : null);
|
||||
if (expiry != null && Number(expiry) === Number(user.starter_pack)
|
||||
&& (order.starterPackBuyRound == null || Number(order.starterPackBuyRound) === (Number(user.starterPackRound) || 1))) {
|
||||
await db.collection("users").where({ _id: user._id, starter_pack: user.starter_pack,
|
||||
starterPackRound: user.starterPackRound ?? null }).update({ starter_packState: 1 });
|
||||
}
|
||||
userUpdate.starter_packState = 1;
|
||||
}
|
||||
if (!user.pay_user) {
|
||||
userUpdate.pay_user = true;
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@
|
|||
|
||||
## 支付及兼容
|
||||
|
||||
现有下单接口仅对上述五个 SKU 校验当前有效轮次、未购买状态与商品匹配,并固定服务端报价和数量 1。订单保存不含奖励内容的 `starterPackOffer`、`starterPackBuyRound`、`starterPackExpiresAt`、`activityType`、`rewardVersion`,旧 iOS 转单继续携带这些字段。
|
||||
当前临时关闭新手礼包的服务端档位价格、轮次和购买资格校验。五个有效 SKU 使用客户端提交的价格与数量创建订单;`starter_pack_1` 等无效 SKU 仍拒绝。订单不保存 `starterPackOffer`、`starterPackBuyRound` 或 `starterPackExpiresAt`,只保留画像统计使用的 `activityType` 和 `rewardVersion`。
|
||||
|
||||
新订单到期后不可创建;到期前的有效订单可继续原支付流程。回调只关闭所属轮次,不关闭后开的礼包。查单携带商品和轮次快照;`getOrderReward` 仍返回 `data="ok"`,礼包快照位于响应顶层。前端只按订单商品 ID 读取本地固定奖励,不读取后端奖励字段。登录和账号迁移保留轮次及报价。
|
||||
支付回调和登录补单确认任一新手礼包 SKU 后,直接把当前礼包标记为已购买,不匹配订单轮次。查单和 `getOrderReward` 只返回订单商品 ID,前端按商品 ID 读取本地固定奖励。活动展示本身仍保留当前轮次及报价,用于客户端展示和触发下一轮。
|
||||
|
||||
保留原跨设备并发下单、客户端发奖及旧 iOS 响应中断的边界,未新增订单占位锁或服务端发奖事务。礼包待支付订单不再按原通用 3 小时期限清理,以免删除仍可能支付的旧单;关闭但未成功的礼包订单也会保留,不因此发奖。退款不在本次范围内。
|
||||
|
||||
`rookie_gift` 支付成功埋点扩展至五个 SKU,沿用订单级去重;轮次和剩余秒数取原订单快照。
|
||||
`rookie_gift` 支付成功埋点扩展至五个 SKU,沿用订单级去重;新订单不保存活动轮次时按现有保守兼容逻辑推算轮次和剩余秒数。
|
||||
|
||||
## 发布与验证
|
||||
|
||||
|
|
|
|||
|
|
@ -56,25 +56,21 @@ test('disabled gold miner orders never enter legacy iOS recovery', async () => {
|
|||
});
|
||||
|
||||
for (const itemid of ['starter_pack', 'starter_pack_2', 'starter_pack_3', 'starter_pack_4', 'starter_pack_5']) {
|
||||
test(`login preserves lucky offer metadata and recovers ${itemid} through original delivery`, async () => {
|
||||
const offer = { version: 3, round: 2, productId: itemid, expiresAt: Date.now() + 10000,
|
||||
priceFen: 1200, reward: { coin: 4500, hammer: 3, freeze: 3, magic_wand: 3, infinite_health: 2700 } };
|
||||
const { user, order, recover } = fixture(itemid, {
|
||||
starterPackOffer: offer, starterPackBuyRound: 2, starterPackExpiresAt: offer.expiresAt,
|
||||
});
|
||||
Object.assign(user, { starterPackOffer: offer, starterPackRound: 2, starter_pack: offer.expiresAt });
|
||||
test(`login recovers ${itemid} without order offer or cycle metadata`, async () => {
|
||||
const { user, order, recover } = fixture(itemid);
|
||||
const pending = await recover(user.openid, {});
|
||||
assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1);
|
||||
assert.deepEqual(pending[0].starterPackOffer, offer);
|
||||
assert.equal(pending[0].itemid, itemid); assert.equal(pending[0].starterPackOffer, undefined);
|
||||
assert.equal(pending[0].starterPackBuyRound, undefined); assert.equal(pending[0].starterPackExpiresAt, undefined);
|
||||
assert.deepEqual(await recover(user.openid, {}), []);
|
||||
});
|
||||
}
|
||||
|
||||
test('login payment for an earlier lucky round never closes the current offer', async () => {
|
||||
test('login payment closes the current offer without matching an order round', async () => {
|
||||
const { user, order, recover } = fixture('starter_pack_3', { starterPackBuyRound: 2, starterPackExpiresAt: 1000 });
|
||||
Object.assign(user, { starterPackRound: 3, starter_pack: 2000, starterPackOffer: { version: 3, round: 3 } });
|
||||
assert.equal((await recover(user.openid, {})).length, 1);
|
||||
assert.equal(user.starter_packState, undefined); assert.equal(user.pay_user, true); assert.equal(order.state, 1);
|
||||
assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true); assert.equal(order.state, 1);
|
||||
});
|
||||
|
||||
test('login closes a matching migrated legacy cycle for an older order without an offer snapshot', async () => {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ registerHooks({
|
|||
return next(specifier, context);
|
||||
}
|
||||
});
|
||||
const { reportRookieGift, starterPackOrderSnapshot } = await import('../functions/limitedTimeEvent.ts');
|
||||
const { reportRookieGift, starterPackOrderMetadata } = await import('../functions/limitedTimeEvent.ts');
|
||||
const { default: callback } = await import('../functions/wx/payCallBack.ts');
|
||||
const { default: query } = await import('../functions/wx/getPayInfo.ts');
|
||||
const { default: iosQuery } = await import('../functions/wx/iosgetPayInfo.ts');
|
||||
|
|
@ -73,7 +73,8 @@ function reset() {
|
|||
tables.users.push(user); return user;
|
||||
}
|
||||
function order(user, extra = {}) {
|
||||
const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: 'starter_pack', itemCount: 1, goodsPrice: 300, state: 0, time: NOW - 10000, ...starterPackOrderSnapshot(user, 'starter_pack'), ...extra };
|
||||
const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: 'starter_pack', itemCount: 1, goodsPrice: 300,
|
||||
state: 0, time: NOW - 10000, ...starterPackOrderMetadata('starter_pack'), ...extra };
|
||||
tables.order.push(row); return row;
|
||||
}
|
||||
const ctx = body => ({ body, headers: {}, socket: { remoteAddress: '127.0.0.1' } });
|
||||
|
|
@ -87,83 +88,88 @@ function setOffer(user, productId, priceFen) {
|
|||
return user.starterPackOffer;
|
||||
}
|
||||
|
||||
test('each lucky tier uses the server offer price, quantity and tier snapshot on native payment', async () => {
|
||||
test('each lucky tier keeps the submitted price and quantity without an offer or cycle snapshot', async () => {
|
||||
for (const create of [androidOrder, nativeIosOrder]) {
|
||||
for (const [productId, priceFen] of [['starter_pack', 300], ['starter_pack_2', 600], ['starter_pack_3', 1200], ['starter_pack_4', 1800], ['starter_pack_5', 3000]]) {
|
||||
const user = reset(), offer = setOffer(user, productId, priceFen);
|
||||
const result = await create(ctx({ uid: 'u', itemid: productId, itemPrice: 1, itemCount: 9 }));
|
||||
assert.equal(result.code, 1);
|
||||
const saved = tables.order[0];
|
||||
assert.equal(saved.goodsPrice, priceFen); assert.equal(saved.itemCount, 1);
|
||||
assert.deepEqual(saved.starterPackOffer, offer);
|
||||
assert.equal(saved.starterPackOffer.reward, undefined);
|
||||
assert.equal(saved.goodsPrice, 1); assert.equal(saved.itemCount, 9);
|
||||
assert.equal(saved.starterPackOffer, undefined);
|
||||
assert.equal(saved.starterPackBuyRound, undefined); assert.equal(saved.starterPackExpiresAt, undefined);
|
||||
assert.equal(saved.activityType, 'lucky'); assert.equal(saved.rewardVersion, offer.rewardVersion);
|
||||
const signed = JSON.parse(result.data.signData);
|
||||
assert.equal(signed.productId, productId); assert.equal(signed.goodsPrice, priceFen); assert.equal(signed.buyQuantity, 1);
|
||||
assert.equal(signed.productId, productId); assert.equal(signed.goodsPrice, 1); assert.equal(signed.buyQuantity, 9);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('lucky native creation rejects a mismatched tier, invalid suffix, expired or purchased offer', async () => {
|
||||
for (const create of [androidOrder, nativeIosOrder]) for (const scenario of ['mismatch', 'suffix', 'expired', 'purchased']) {
|
||||
test('lucky native creation accepts any valid tier without offer eligibility checks', async () => {
|
||||
for (const create of [androidOrder, nativeIosOrder]) for (const scenario of ['mismatch', 'expired', 'purchased']) {
|
||||
const user = reset(); setOffer(user, 'starter_pack_3', 1200);
|
||||
if (scenario === 'expired') { user.starter_pack = NOW; user.starterPackOffer.expiresAt = NOW; }
|
||||
if (scenario === 'purchased') user.starter_packState = 1;
|
||||
const itemid = scenario === 'mismatch' ? 'starter_pack_2' : scenario === 'suffix' ? 'starter_pack_1' : 'starter_pack_3';
|
||||
assert.equal((await create(ctx({ uid: 'u', itemid, itemPrice: 1200, itemCount: 1 }))).code, 0);
|
||||
const itemid = scenario === 'mismatch' ? 'starter_pack_2' : 'starter_pack_3';
|
||||
assert.equal((await create(ctx({ uid: 'u', itemid, itemPrice: 1200, itemCount: 1 }))).code, 1);
|
||||
assert.equal(tables.order.length, 1);
|
||||
}
|
||||
for (const create of [androidOrder, nativeIosOrder]) {
|
||||
reset();
|
||||
assert.equal((await create(ctx({ uid: 'u', itemid: 'starter_pack_1', itemPrice: 300, itemCount: 1 }))).code, 0);
|
||||
assert.equal(tables.order.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('lucky query and original reward acknowledgement return the purchased snapshot after expiry', async () => {
|
||||
test('lucky query and reward acknowledgement return only the purchased SKU', async () => {
|
||||
const user = reset(), offer = setOffer(user, 'starter_pack_4', 1800);
|
||||
const row = { outTradeNo: 'wcx_test', openid: user.openid, itemid: offer.productId, goodsPrice: offer.priceFen,
|
||||
itemCount: 1, state: 1, ...starterPackOrderSnapshot(user, offer.productId) };
|
||||
itemCount: 1, state: 1, ...starterPackOrderMetadata(offer.productId) };
|
||||
tables.order.push(row); now += 2 * DAY;
|
||||
const result = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo }));
|
||||
assert.equal(result.data.pay_state, 2); assert.equal(result.data.itemid, offer.productId);
|
||||
assert.deepEqual(result.data.starterPackOffer, offer);
|
||||
assert.equal(result.data.starterPackOffer, undefined);
|
||||
const claimed = await claimOrder(ctx({ uid: 'u', outTradeNo: row.outTradeNo }));
|
||||
assert.equal(claimed.data, 'ok'); assert.deepEqual(claimed.starterPackOffer, offer); assert.equal(row.state, 2);
|
||||
assert.equal(claimed.data, 'ok'); assert.equal(claimed.itemid, offer.productId); assert.equal(row.state, 2);
|
||||
const done = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo }));
|
||||
assert.equal(done.data.pay_state, 1); assert.deepEqual(done.data.starterPackOffer, offer);
|
||||
assert.equal(done.data.pay_state, 1); assert.equal(done.data.itemid, offer.productId);
|
||||
});
|
||||
|
||||
test('lucky callback marks only its own round, while historical orders still confirm payment', async () => {
|
||||
test('lucky callback closes the current offer without matching an order round', async () => {
|
||||
for (const older of [false, true]) {
|
||||
const user = reset(), offer = setOffer(user, 'starter_pack_5', 3000);
|
||||
const row = { outTradeNo: 'wcx_test', openid: 'o', itemid: offer.productId, goodsPrice: 3000,
|
||||
itemCount: 1, time: NOW, state: 0, ...starterPackOrderSnapshot(user, offer.productId) };
|
||||
itemCount: 1, time: NOW, state: 0, ...starterPackOrderMetadata(offer.productId) };
|
||||
tables.order.push(row);
|
||||
if (older) { user.starter_pack += DAY; user.starterPackRound++; setOffer(user, 'starter_pack_2', 600); }
|
||||
assert.equal((await callback(notification())).ErrCode, 0); assert.equal(row.state, 1);
|
||||
assert.equal(user.starter_packState === 1, !older); assert.equal(user.pay_user, true);
|
||||
assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true);
|
||||
}
|
||||
const user = reset(), row = order(user);
|
||||
delete row.starterPackOffer; delete row.starterPackExpiresAt; delete row.starterPackBuyRound;
|
||||
user.starterPackRound++; user.starter_pack += DAY; setOffer(user, 'starter_pack_2', 600);
|
||||
assert.equal((await callback(notification())).ErrCode, 0);
|
||||
assert.notEqual(user.starter_packState, 1); assert.equal(row.state, 1);
|
||||
assert.equal(user.starter_packState, 1); assert.equal(row.state, 1);
|
||||
const result = await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo }));
|
||||
assert.equal(result.data.starterPackOffer, undefined);
|
||||
});
|
||||
|
||||
test('legacy iOS lucky tiers preserve the locked offer through creation, transfer and paid query', async () => {
|
||||
test('legacy iOS lucky tiers keep submitted price and SKU without an offer snapshot', async () => {
|
||||
const user = reset(), offer = setOffer(user, 'starter_pack_3', 1200);
|
||||
shared.set('wxaccess_token', 'synthetic'); provider = { prepay_id: 'mock-prepay' };
|
||||
await customerService(ctx({ FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios',
|
||||
outTradeNo: 'tier-ios', propName: offer.productId, count: 9, price: 1 }) }));
|
||||
assert.equal(tables.iosOrder[0].goodsPrice, 1200); assert.equal(tables.iosOrder[0].itemCount, 1);
|
||||
assert.deepEqual(tables.iosOrder[0].starterPackOffer, offer);
|
||||
assert.equal(tables.iosOrder[0].goodsPrice, 1); assert.equal(tables.iosOrder[0].itemCount, 9);
|
||||
assert.equal(tables.iosOrder[0].starterPackOffer, undefined);
|
||||
assert.equal(tables.iosOrder[0].activityType, 'lucky');
|
||||
assert.equal(await transfer(ctx({ openid: 'o', time: user.iosTime, outTradeNo: 'tier-ios' })), true);
|
||||
assert.deepEqual(tables.order[0].starterPackOffer, offer);
|
||||
assert.equal(tables.order[0].starterPackOffer, undefined); assert.equal(tables.order[0].activityType, 'lucky');
|
||||
user.starter_pack += DAY; user.starterPackRound++; setOffer(user, 'starter_pack_5', 3000);
|
||||
provider = { trade_state: 'SUCCESS', success_time: new Date(NOW).toISOString() };
|
||||
const paid = await iosQuery(ctx({ uid: 'u', outTradeNo: 'tier-ios' }));
|
||||
assert.equal(paid.code, 1); assert.deepEqual(paid.data.starterPackOffer, offer);
|
||||
assert.notEqual(user.starter_packState, 1); assert.equal(tables.order[0].state, 2);
|
||||
assert.equal(paid.code, 1); assert.equal(paid.data.itemid, offer.productId);
|
||||
assert.equal(user.starter_packState, 1); assert.equal(tables.order[0].state, 2);
|
||||
const done = await iosQuery(ctx({ uid: 'u', outTradeNo: 'tier-ios' }));
|
||||
assert.equal(done.msg, '已经获取到奖励'); assert.deepEqual(done.data.starterPackOffer, offer);
|
||||
assert.equal(done.msg, '已经获取到奖励'); assert.equal(done.data.itemid, offer.productId);
|
||||
});
|
||||
|
||||
test('old orders without snapshots close only a matching migrated legacy offer', async () => {
|
||||
|
|
@ -181,23 +187,23 @@ test('old orders without snapshots close only a matching migrated legacy offer',
|
|||
}
|
||||
});
|
||||
|
||||
test('native Android and iOS orders snapshot the server cycle, ignoring client analytics values', async () => {
|
||||
test('native Android and iOS orders do not retain client or server cycle fields', async () => {
|
||||
for (const create of [androidOrder, nativeIosOrder]) {
|
||||
const user = reset();
|
||||
const result = await create(ctx({ uid: 'u', itemid: 'starter_pack', itemCount: 1, itemPrice: 300, starterPackBuyRound: 99, starterPackExpiresAt: 1 }));
|
||||
assert.equal(result.code, 1);
|
||||
assert.equal(tables.order[0].starterPackBuyRound, 3);
|
||||
assert.equal(tables.order[0].starterPackExpiresAt, user.starter_pack);
|
||||
assert.equal(tables.order[0].starterPackBuyRound, undefined);
|
||||
assert.equal(tables.order[0].starterPackExpiresAt, undefined);
|
||||
assert.equal(events.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('native callback reports the paid order cycle and integer seconds, not a later reactivation', async () => {
|
||||
test('native callback without a cycle snapshot uses the conservative analytics fallback', async () => {
|
||||
const user = reset(), row = order(user);
|
||||
user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4;
|
||||
assert.equal((await callback(notification())).ErrCode, 0);
|
||||
assert.equal(row.state, 1); assert.notEqual(user.starter_packState, 1); assert.equal(user.pay_user, true);
|
||||
assert.deepEqual(events[0].properties, { order_id: 'wcx_test', buy_round: 3, time_left: 3599 });
|
||||
assert.equal(row.state, 1); assert.equal(user.starter_packState, 1); assert.equal(user.pay_user, true);
|
||||
assert.deepEqual(events[0].properties, { order_id: 'wcx_test', buy_round: 1, time_left: 0 });
|
||||
assert.equal(events[0].event, 'rookie_gift'); assert.equal(events[0].accountId, 'o');
|
||||
assert.equal(events[0].distinctId, 'd'); assert.equal(events[0].firstCheckId, 'wcx_test');
|
||||
});
|
||||
|
|
@ -216,7 +222,7 @@ test('concurrent and repeated callbacks and paid queries keep a single dedup key
|
|||
|
||||
test('payment at or beyond expiry still succeeds and reports zero remaining seconds', async () => {
|
||||
for (const expiry of [NOW, NOW - 1000]) {
|
||||
const user = reset(); order(user, { starterPackExpiresAt: expiry });
|
||||
const user = reset(); user.starter_pack = expiry; order(user);
|
||||
assert.equal((await callback(notification())).ErrCode, 0);
|
||||
assert.equal(events[0].properties.time_left, 0);
|
||||
}
|
||||
|
|
@ -227,7 +233,7 @@ test('unpaid queries and other products never send rookie_gift', async () => {
|
|||
assert.equal((await query(ctx({ uid: 'u', outTradeNo: row.outTradeNo }))).code, 0);
|
||||
assert.equal(events.length, 0);
|
||||
row.itemid = 'gold_1'; await callback(notification()); assert.equal(events.length, 0);
|
||||
assert.deepEqual(starterPackOrderSnapshot(user, 'gold_1'), {});
|
||||
assert.deepEqual(starterPackOrderMetadata('gold_1'), {});
|
||||
});
|
||||
|
||||
test('SDK failure cannot block payment; a later query retries the original snapshot', async () => {
|
||||
|
|
@ -254,23 +260,22 @@ test('existing ThinkingData debug routing is retained and account ID works witho
|
|||
|
||||
test('legacy pending orders use cycle one; missing historical deadline falls back to zero', async () => {
|
||||
const user = reset(); delete user.starterPackRound;
|
||||
const row = order(user); delete row.starterPackBuyRound; delete row.starterPackExpiresAt;
|
||||
const row = order(user);
|
||||
await reportRookieGift(row, user, NOW);
|
||||
assert.equal(events[0].properties.buy_round, 1); assert.equal(events[0].properties.time_left, 3599);
|
||||
reset(); const older = order(user, { time: NOW - 5 * DAY });
|
||||
delete older.starterPackBuyRound; delete older.starterPackExpiresAt;
|
||||
await reportRookieGift(older, user, NOW);
|
||||
assert.equal(events[0].properties.time_left, 0);
|
||||
});
|
||||
|
||||
test('legacy iOS creation and transfer preserve cycle snapshot even if the user later changes', async () => {
|
||||
test('legacy iOS creation and transfer omit cycle snapshots', async () => {
|
||||
const user = reset(); shared.set('wxaccess_token', 'synthetic'); provider = { prepay_id: 'mock-prepay' };
|
||||
await customerService(ctx({ FromUserName: 'o', SessionFrom: JSON.stringify({ tpye: 'ios', outTradeNo: 'old-ios', propName: 'starter_pack', count: 1, price: 300 }) }));
|
||||
assert.equal(tables.iosOrder.length, 1);
|
||||
assert.equal(tables.iosOrder[0].starterPackBuyRound, 3);
|
||||
const expiry = user.starter_pack; user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4;
|
||||
assert.equal(tables.iosOrder[0].starterPackBuyRound, undefined);
|
||||
user.starter_pack = NOW + 2 * DAY; user.starterPackRound = 4;
|
||||
assert.equal(await transfer(ctx({ openid: 'o', time: user.iosTime, outTradeNo: 'old-ios' })), true);
|
||||
assert.equal(tables.order[0].starterPackBuyRound, 3); assert.equal(tables.order[0].starterPackExpiresAt, expiry);
|
||||
assert.equal(tables.order[0].starterPackBuyRound, undefined); assert.equal(tables.order[0].starterPackExpiresAt, undefined);
|
||||
assert.equal(events.length, 0);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ registerHooks({
|
|||
return next(url, context);
|
||||
},
|
||||
});
|
||||
const { default: activity, starterPackPatch, starterPackOrderSnapshot } = await import('../functions/limitedTimeEvent.ts');
|
||||
const { default: activity, starterPackPatch, starterPackOrderMetadata } = await import('../functions/limitedTimeEvent.ts');
|
||||
const { PAYMENT_PROFILE_RULE } = await import('../functions/paymentProfile.ts');
|
||||
const { starterPackReward } = await import('../../../assets/Script/module/Config/StarterPack.ts');
|
||||
function reset(patch = {}) {
|
||||
|
|
@ -134,7 +134,6 @@ test('a displayed offer stays fixed when payment identity and daily factors chan
|
|||
user.pay_user = true; profile(user, 100000, 100000); now += 3600000;
|
||||
await read(); await trigger();
|
||||
assert.deepEqual(user.starterPackOffer, offer);
|
||||
assert.equal(starterPackOrderSnapshot(user, 'starter_pack').starterPackOffer.priceFen, 300);
|
||||
});
|
||||
|
||||
test('legacy active offers retain their original deadline and use the client 3 yuan reward', async () => {
|
||||
|
|
@ -231,18 +230,13 @@ test('form-encoded failure counts and automatic flags match the client transport
|
|||
}
|
||||
});
|
||||
|
||||
test('order snapshots bind a valid current SKU, price and cycle without reward data', async () => {
|
||||
const user = reset({ pay_user: true }); profile(user, 3000); await trigger();
|
||||
const snapshot = starterPackOrderSnapshot(user, 'starter_pack_5');
|
||||
assert.equal(snapshot.starterPackOffer.priceFen, 3000);
|
||||
assert.equal(snapshot.starterPackOffer.reward, undefined);
|
||||
assert.equal(snapshot.activityType, 'lucky');
|
||||
assert.equal(snapshot.rewardVersion, PAYMENT_PROFILE_RULE.starterPackRewardVersion);
|
||||
assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack'), /档位/);
|
||||
user.starter_packState = 1; assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /已购买/);
|
||||
user.starter_packState = 0; now = user.starter_pack;
|
||||
assert.throws(() => starterPackOrderSnapshot(user, 'starter_pack_5'), /未开放/);
|
||||
assert.deepEqual(starterPackOrderSnapshot(user, 'other_product'), {});
|
||||
test('payment metadata keeps factor labels without price, round or eligibility checks', () => {
|
||||
for (const itemid of ['starter_pack', 'starter_pack_2', 'starter_pack_3', 'starter_pack_4', 'starter_pack_5']) {
|
||||
assert.deepEqual(starterPackOrderMetadata(itemid), {
|
||||
activityType: 'lucky', rewardVersion: PAYMENT_PROFILE_RULE.starterPackRewardVersion,
|
||||
});
|
||||
}
|
||||
assert.deepEqual(starterPackOrderMetadata('other_product'), {});
|
||||
});
|
||||
|
||||
test('formula ignores rejection state and historical successful price', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user