Files
AriWeb-Suite/test/e2e/flows/65-usage.mjs

25 lines
5.3 KiB
JavaScript

/** 흐름 18(+13.2/13.3 보조): 상품 축소 미리보기 → 보류(정리 목록) → 배정 축소 정리 → 재검증 → 축소 주문·승인 → 권리 교체. 배정 변경 동시성도 확인. */
import { steps } from '../report.mjs';
import { signupVerified } from './10-identity.mjs';
import { orderAndApprove } from './20-commerce.mjs';
import { cancelDomainJob } from './30-sites.mjs';
export const BUDGET_MS = 40000;
export async function run({ env, report, state }) {
const alice = state.alice; if (!alice || !state.mailServiceId) { report.flow(18, 'partial', { note: '선행 흐름 없음', blockedBy: 10 }); return; }
const s = steps();
try {
const basic = (await alice.get('/api/commerce/products')).json.products.find((p) => p.productId === 'basic');
const alloc = await s.run('배정 화면 수치(구매·내 배정·다른 서비스·예약·미배정·변경 후 최대) + 마지막 단위 동시 요청', async () => { const sub = `mail_service:${state.mailServiceId}`; const r = await alice.get(`/api/usage/allocate/${sub}`); const mb = r.json.resources?.find((x) => x.resource === 'mailboxes'); const [a, b] = await Promise.all([alice.post(`/api/usage/allocate/${sub}`, { resource: 'mailboxes', amount: mb.purchased }), alice.post(`/api/usage/allocate/${sub}`, { resource: 'mailboxes', amount: mb.purchased })]); return { purchased: mb?.purchased, mine: mb?.mine, others: mb?.others, unallocated: mb?.unallocated, race: [a.status, b.status].sort(), mineAfter: (await alice.get(`/api/usage/allocate/${sub}`)).json.resources?.find((x) => x.resource === 'mailboxes')?.mine }; });
// 축소는 별도 고객(캐럴: 팀 상품 + 대표 메일 6개 배정)으로 — 앨리스 그룹의 백업·독립 메일 보존 상태와 섞이지 않게
const carol = await signupVerified(env, 'carol@e2e.localhost', '캐럴'); await orderAndApprove(env, carol, { productId: 'team', key: 'e2e-carol-team' });
const csite = (await carol.post('/api/sites', { name: '캐럴 사이트', projectMode: 'none' })).json.site; const cd = await carol.post('/api/domains', { name: 'carol-e2e.localhost', purpose: 'web', siteId: csite.id, startCheck: false }); if (cd.json?.domain?.id) await cancelDomainJob(carol, cd.json.domain.id);
const cm = await carol.post('/api/mail/services', { kind: 'site_bound', siteId: csite.id, domain: 'carol-e2e.localhost', alloc: { mailboxes: 6, storageBytes: 1024 ** 3, monthlyMessages: 100 } }); if (cm.status !== 200) throw new Error('캐럴 메일 생성 실패 ' + cm.text.slice(0, 200));
const pv = await s.run('축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시', async () => { const r = await carol.post('/api/commerce/downsize/preview', { productVersionId: basic.id }); if (r.status !== 200) throw new Error(r.text.slice(0, 300)); return { fits: r.json.fits, shortages: (r.json.shortages || []).map((x) => x.resource + ':' + x.reduceBy) }; });
const hold = await s.run('축소 요청 → 보류(사용 중 권리 즉시 회수 없음)', async () => { const r = await carol.post('/api/commerce/downsize/request', { productVersionId: basic.id, acceptTerms: true }); if (r.status !== 200) throw new Error(r.text.slice(0, 300)); const ent = (await carol.get('/api/commerce/billing')).json.entitlements?.find((e) => e.resource === 'sites'); return { fits: r.json.fits, hold: r.json.hold?.status, sitesStillPurchased: ent?.purchased }; });
const clean = await s.run('정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성', async () => { const a = await carol.post(`/api/usage/allocate/mail_service:${cm.json.service.id}`, { resource: 'mailboxes', amount: 5 }); const r = await carol.post('/api/commerce/downsize/revalidate'); return { allocate: a.status, allocateError: a.json?.error, fits: r.json.fits, shortages: (r.json.shortages || []).map((x) => x.resource + ':' + x.reduceBy), order: r.json.order?.status }; });
let applied = null;
if (clean.fits && clean.order) { applied = await s.run('축소 주문 입금 확인·승인 → 권리 교체(베이직)', async () => { const o = (await carol.get('/api/commerce/orders')).json.orders.find((x) => x.kind === 'downsize'); await env.relay('POST', `/commerce/orders/${o.id}/paid`, { ref: 'e2e' }); const ap = await env.relay('POST', `/commerce/orders/${o.id}/approve`, {}); const b = await carol.get('/api/commerce/billing'); return { approve: ap.status, sites: b.json.entitlements?.find((e) => e.resource === 'sites')?.purchased, mailboxes: b.json.entitlements?.find((e) => e.resource === 'mailboxes')?.purchased, product: b.json.subscriptions?.[0]?.product_name || b.json.subscriptions?.[0]?.product_version_id }; }); state.downsized = true; }
report.flow(18, applied ? 'verified_local' : 'partial', { steps: s.list, note: applied ? '축소 미리보기→보류→정리(배정 축소·독립 메일 해지)→재검증 적합→축소 주문·승인→권리 교체' : '축소 보류와 정리 목록까지 확인(재검증 결과는 증거 참조)', evidence: { alloc, pv, hold, clean, applied } });
} catch (e) { report.flow(18, 'failed', { steps: s.list, note: e.message }); report.bug({ title: '상품 축소 보류·재검증 흐름 실패', file: 'portal/commerce/api.mjs', route: '/api/commerce/downsize/*', expected: '보류→정리→적용', actual: e.message, repro: '65-usage.mjs', flows: [18] }); }
}