onboard never writes duplicate subscription levels: the App Store groupLevel when it is set and unique in the group, else a stable order (App Store level, higher price per month = higher level, longer period, id) with a note per derived level; assignLevels in cli/lib/onboard.ts, tests with a live-shape App Store group (groupLevel 2 and 1) and Play-only / tied cases
This commit is contained in:
parent
346bf61166
commit
5ad3c0002d
2 changed files with 80 additions and 1 deletions
|
|
@ -27,9 +27,23 @@ const money = (currencyCode: string, units: string, nanos = 0) => ({ currencyCod
|
|||
|
||||
/** App Store Connect + Google Play as they answered for a live app (sanitized): Pro has its price schedule's base
|
||||
* territory in Turkey, Tips is on the App Store only, Plus monthly is a Play-only subscription. */
|
||||
function liveStores(o: { review?: Record<string, unknown>; draft?: boolean } = {}): Call[] {
|
||||
function liveStores(o: { review?: Record<string, unknown>; draft?: boolean; subs?: boolean } = {}): Call[] {
|
||||
const P = '/androidpublisher/v3/applications/com.example.notes';
|
||||
// with `subs`: one App Store group "Premium" whose monthly (groupLevel 2) is listed before its yearly (groupLevel 1)
|
||||
const subPrice = (id: string, pt: string) => ({ type: 'subscriptionPrices', id, attributes: { startDate: null, preserved: false }, relationships: { subscriptionPricePoint: { data: { type: 'subscriptionPricePoints', id: pt } }, territory: { data: { type: 'territories', id: 'USA' } } } });
|
||||
const appleSubs = o.subs ? [
|
||||
{ match: /^\/v1\/apps\/6400000101\/subscriptionGroups/, answer: page([{ type: 'subscriptionGroups', id: 'g1', attributes: { referenceName: 'Premium' } }]) },
|
||||
{ match: /^\/v1\/subscriptionGroups\/g1\/subscriptions/, answer: page([
|
||||
{ type: 'subscriptions', id: 's-m', attributes: { productId: 'com.example.notes.premium.monthly', name: 'Premium monthly', state: 'APPROVED', subscriptionPeriod: 'ONE_MONTH', groupLevel: 2, familySharable: false } },
|
||||
{ type: 'subscriptions', id: 's-y', attributes: { productId: 'com.example.notes.premium.yearly', name: 'Premium yearly', state: 'APPROVED', subscriptionPeriod: 'ONE_YEAR', groupLevel: 1, familySharable: false } }]) },
|
||||
{ match: /^\/v1\/subscriptions\/s-m\/subscriptionLocalizations/, answer: page([{ type: 'subscriptionLocalizations', id: 'sl1', attributes: { locale: 'en-US', name: 'Premium monthly', description: 'Everything, monthly' } }]) },
|
||||
{ match: /^\/v1\/subscriptions\/s-y\/subscriptionLocalizations/, answer: page([{ type: 'subscriptionLocalizations', id: 'sl2', attributes: { locale: 'en-US', name: 'Premium yearly', description: 'Everything, yearly' } }]) },
|
||||
{ match: /^\/v1\/subscriptions\/s-m\/prices/, answer: page([subPrice('sp1', 'spp-m')], [{ type: 'subscriptionPricePoints', id: 'spp-m', attributes: { customerPrice: '4.99', proceeds: '3.49' } }, terr('USA', 'USD')]) },
|
||||
{ match: /^\/v1\/subscriptions\/s-y\/prices/, answer: page([subPrice('sp2', 'spp-y')], [{ type: 'subscriptionPricePoints', id: 'spp-y', attributes: { customerPrice: '39.99', proceeds: '27.99' } }, terr('USA', 'USD')]) },
|
||||
{ match: /^\/v1\/subscriptions\/s-[my]\/(introductoryOffers|promotionalOffers|offerCodes)/, answer: page([]) },
|
||||
] : [];
|
||||
return mockFetch([
|
||||
...appleSubs,
|
||||
// ---- App Store Connect: catalog
|
||||
{ match: /^\/v1\/apps\/6400000101\/inAppPurchasesV2\?limit=200/, answer: page([
|
||||
{ type: 'inAppPurchases', id: 'i-pro', attributes: { productId: PRO, name: 'Pro', inAppPurchaseType: 'NON_CONSUMABLE', state: 'APPROVED' } },
|
||||
|
|
@ -164,6 +178,38 @@ describe('contact, sign-in, deletion page (pure)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('subscription levels', () => {
|
||||
test('App Store groupLevel is imported per subscription (live shape), the Play-only one gets the next free level', async () => {
|
||||
ascEnv(); playEnv();
|
||||
const p = liveProject();
|
||||
liveStores({ subs: true });
|
||||
const r = await capture(() => onboard(p.ctx(['--write'], ['write'])));
|
||||
expect(r.exit).toBeNull();
|
||||
const c = (await loadConfig(p.loaded.path)).config;
|
||||
const g = c.subscriptions!.find(x => x.name === 'Premium')!;
|
||||
expect(g.subscriptions.map(x => `${x.id} ${x.level}`)).toEqual(['com.example.notes.premium.monthly 2', 'com.example.notes.premium.yearly 1']);
|
||||
});
|
||||
test('Play-only and tied levels: price per month (higher = level 1), then the longer period; never duplicated, with a note', () => {
|
||||
const play: StoreItem[] = [
|
||||
{ id: 'a.basic', kind: 'subscription', period: 'P1M', locales: ['en-US'], prices: [{ region: 'US', currency: 'USD', price: 2.99 }] },
|
||||
{ id: 'a.pro', kind: 'subscription', period: 'P1M', locales: ['en-US'], prices: [{ region: 'US', currency: 'USD', price: 9.99 }] },
|
||||
{ id: 'a.pro.year', kind: 'subscription', period: 'P1Y', locales: ['en-US'], prices: [{ region: 'US', currency: 'USD', price: 119.88 }] },
|
||||
];
|
||||
const playTexts = Object.fromEntries(play.map(x => [x.id, { 'en-US': { title: x.id, description: 'd' } }]));
|
||||
const cfg = { ...BASE, apple: undefined, subscriptions: [{ name: 'Subscriptions', listings: { en: { name: 'Subscriptions' } }, subscriptions: [
|
||||
{ id: 'a.basic', period: 'P1M' as const, level: 1, usd: 2.99, listings: { en: { title: 'a.basic', description: 'd' } } },
|
||||
{ id: 'a.pro', period: 'P1M' as const, level: 1, usd: 9.99, listings: { en: { title: 'a.pro', description: 'd' } } }] }] };
|
||||
const r = importCatalog(cfg, { apple: null, play, playTexts });
|
||||
expect(r.subscriptions[0]!.subscriptions.map(x => `${x.id} ${x.level}`).sort()).toEqual(['a.basic 3', 'a.pro 2', 'a.pro.year 1']);
|
||||
expect(r.notes.filter(n => n.msg.includes('ordered by price per month')).length).toBe(3);
|
||||
// ties on the App Store are broken the same way
|
||||
const apple: StoreItem[] = play.map(x => ({ ...x, level: 1, group: 'Subscriptions' }));
|
||||
const t = importCatalog({ ...cfg, apple: BASE.apple, play: undefined }, { apple, play: null, appleTexts: playTexts });
|
||||
expect(t.subscriptions[0]!.subscriptions.map(x => `${x.id} ${x.level}`).sort()).toEqual(['a.basic 3', 'a.pro 2', 'a.pro.year 1']);
|
||||
expect(t.notes.some(n => n.msg.includes('groupLevel 1 is shared'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storebound onboard (recorded live shapes)', () => {
|
||||
test('--write: catalog with prices, contact, review contact and demo account (never printed), sign-in, deletion page; catalog diff clean', async () => {
|
||||
ascEnv(); playEnv();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue