test(stage): пункт 8 — SupplierReturn 8/8 ✓ (CRUD+Post+Unpost+ref validation+multi-tenant)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9df8e0123e
commit
74e14ebeb5
|
|
@ -24,7 +24,7 @@
|
||||||
- [x] **4. Loss (Списание)** — UI + LossReason; запрет списания больше остатка; multi-tenant. *(stage-loss.yml: 8/8 ✓, EF8 фикс уже в pt 3)*
|
- [x] **4. Loss (Списание)** — UI + LossReason; запрет списания больше остатка; multi-tenant. *(stage-loss.yml: 8/8 ✓, EF8 фикс уже в pt 3)*
|
||||||
- [x] **5. Transfer (Перемещение)** — два склада, From!=To, atomic post, Unpost без orphan-движений; multi-tenant. *(stage-transfer.yml: 7/7 ✓)*
|
- [x] **5. Transfer (Перемещение)** — два склада, From!=To, atomic post, Unpost без orphan-движений; multi-tenant. *(stage-transfer.yml: 7/7 ✓)*
|
||||||
- [x] **6. Inventory (Инвентаризация)** — bookQty подгрузка, импорт CSV факта, корректирующий StockMovement на diff; multi-tenant. *(stage-inventory.yml: 8/8 ✓; logic gap — нет CSV-импорта)*
|
- [x] **6. Inventory (Инвентаризация)** — bookQty подгрузка, импорт CSV факта, корректирующий StockMovement на diff; multi-tenant. *(stage-inventory.yml: 8/8 ✓; logic gap — нет CSV-импорта)*
|
||||||
- [ ] **7. CustomerReturn (Возврат от покупателя)** — UI из RetailSale, по чеку и без; возврат больше продано → 400.
|
- [x] **7. CustomerReturn (Возврат от покупателя)** — UI из RetailSale, по чеку и без; возврат больше продано → 400. *(stage-customer-return.yml: 6/6 ✓)*
|
||||||
- [ ] **8. SupplierReturn (Возврат поставщику)** — аналог для Supply.
|
- [ ] **8. SupplierReturn (Возврат поставщику)** — аналог для Supply.
|
||||||
- [ ] **9. Demand (Оптовая отгрузка)** — юрлицо, опт-цена, кредит (PaidAmount<Total), Stock минус WholesaleSale; multi-tenant.
|
- [ ] **9. Demand (Оптовая отгрузка)** — юрлицо, опт-цена, кредит (PaidAmount<Total), Stock минус WholesaleSale; multi-tenant.
|
||||||
- [ ] **10. Отчёты** — Sales / Stock-на-дату / Profit / ABC; фильтры периода/магазина/группы; экспорт CSV/XLSX; edge.
|
- [ ] **10. Отчёты** — Sales / Stock-на-дату / Profit / ABC; фильтры периода/магазина/группы; экспорт CSV/XLSX; edge.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# E2E report: stage-supplier-return
|
||||||
|
|
||||||
|
Запущен: 2026-05-29T12:21:13.101Z
|
||||||
|
Длительность: 11.1с
|
||||||
|
|
||||||
|
**Итог:** 8 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 8)
|
||||||
|
|
||||||
|
## ✓ Step sr01_setup: 2 org. У orgA — supplier (Legal), продукт, проведённый Supply qty=50 → Stock=50
|
||||||
|
|
||||||
|
Длительность: 8502мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | Initial Stock=50 у org A | ✓ 50 |
|
||||||
|
|
||||||
|
## ✓ Step sr02_create_draft: POST /api/purchases/supplier-returns Draft → 201; total корректный
|
||||||
|
|
||||||
|
Длительность: 473мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | POST supplier-returns → 201 | ✓ 201 {"id":"5135b22b-b5da-46ac-8492-b19ed37e085e","number":"ВП-2026-000001","date":"2026-05-29T12:21:21.605Z","status":0,"supplierId":"b307e91e-d7e6-4cb6-a5f7-c7e446c68cc5","supplierName":"ТОО Поставщи |
|
||||||
|
| api | Total = 5*60 = 300 | ✓ total=300 |
|
||||||
|
| api | Status = Draft (0) | ✓ status=0 |
|
||||||
|
|
||||||
|
## ✓ Step sr03_update_draft: PUT — заменить строки (фикс EF8 ExecuteDelete+DbSet.Add) → 204
|
||||||
|
|
||||||
|
Длительность: 386мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | PUT supplier-return → 204 (фикс EF8) | ✓ 204 |
|
||||||
|
| api | После PUT: total=600 | ✓ total=600 |
|
||||||
|
|
||||||
|
## ✓ Step sr04_post_stock_down: POST /post → 204; Stock -= qty; StockMovement type=SupplierReturn
|
||||||
|
|
||||||
|
Длительность: 486мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | POST /post → 204 | ✓ 204 |
|
||||||
|
| api | Stock -= 10 (50→40) | ✓ 50 → 40 |
|
||||||
|
| api | StockMovement type=SupplierReturn создан | ✓ count=1 type=SupplierReturn |
|
||||||
|
|
||||||
|
## ✓ Step sr05_post_more_than_stock: Возврат больше stock → 409 «Нельзя вернуть больше, чем есть на складе»
|
||||||
|
|
||||||
|
Длительность: 314мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | Возврат > остатка → 409 | ✓ 409 {"error":"Нельзя вернуть больше, чем есть на складе.","lines":[{"productId":"4d39b14e-3912-4e28-a52a-c5dfd301b643","productName":"SR Prod a 1780057273101","returnQty":140,"available":40}]} |
|
||||||
|
|
||||||
|
## ✓ Step sr06_reference_supply_validation: ReferenceSupplyId должен быть проведён, supplier должен совпадать → 400 при несовпадении
|
||||||
|
|
||||||
|
Длительность: 178мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | ReferenceSupplyId с другим supplier → 400 | ✓ 400 {"error":"Поставщик возврата должен совпадать с поставщиком исходной приёмки.","field":"supplierId"} |
|
||||||
|
|
||||||
|
## ✓ Step sr07_unpost: POST /unpost → 204; Stock += обратно
|
||||||
|
|
||||||
|
Длительность: 447мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | POST /unpost → 204 | ✓ 204 |
|
||||||
|
| api | Stock += 10 обратно | ✓ 40 → 50 |
|
||||||
|
| api | Status = Draft (0) | ✓ status=0 |
|
||||||
|
|
||||||
|
## ✓ Step sr08_multi_tenant_isolation: Org B не видит supplier-return org A; POST /post чужого → 404
|
||||||
|
|
||||||
|
Длительность: 342мс
|
||||||
|
|
||||||
|
| Тип | Проверка | Результат |
|
||||||
|
|---|---|---|
|
||||||
|
| api | Org B не видит supplier-return org A | ✓ total=0 sees=false |
|
||||||
|
| api | GET чужого id → 404 | ✓ 404 |
|
||||||
|
| api | POST /post чужого → 404 | ✓ 404 |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- Passed: 8
|
||||||
|
- Failed: 0
|
||||||
|
- Warnings: 0
|
||||||
|
- Skipped: 0
|
||||||
|
|
||||||
|
## Critical bugs
|
||||||
|
|
||||||
|
Нет.
|
||||||
252
tests/e2e/scenarios/stage-supplier-return.steps.ts
Normal file
252
tests/e2e/scenarios/stage-supplier-return.steps.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
/**
|
||||||
|
* Stage SupplierReturn — возврат поставщику. CRUD + Post + Unpost + edge + multi-tenant.
|
||||||
|
*/
|
||||||
|
import { login, makeClient } from '../lib/api.js'
|
||||||
|
import type { CheckResult, Step, Report } from '../lib/report.js'
|
||||||
|
import { generateEan13 } from '../lib/barcode.js'
|
||||||
|
|
||||||
|
type Org = {
|
||||||
|
orgId: string; email: string; password: string; token: string
|
||||||
|
storeId: string; productId: string
|
||||||
|
supplierId: string; supplyId: string
|
||||||
|
currencyId: string; unitKgId: string; groupId: string; priceTypeRetailId: string
|
||||||
|
}
|
||||||
|
type Ctx = {
|
||||||
|
apiOnly: boolean
|
||||||
|
ts: number
|
||||||
|
orgA?: Org
|
||||||
|
orgB?: Org
|
||||||
|
returnIdA?: string
|
||||||
|
otherSupplierIdA?: string
|
||||||
|
}
|
||||||
|
interface StepCtx { ctx: Ctx; step: Step; report: Report }
|
||||||
|
|
||||||
|
const TS = Date.now()
|
||||||
|
function check(step: Step, c: CheckResult) { step.checks.push(c) }
|
||||||
|
function asString(x: unknown): string {
|
||||||
|
if (x == null) return ''
|
||||||
|
if (typeof x === 'string') return x
|
||||||
|
return JSON.stringify(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapOrg(suffix: string, phone: string, bcIdx: number): Promise<Org> {
|
||||||
|
const api = makeClient()
|
||||||
|
const email = `stage-sr-${suffix}-${TS}@food-market.local`
|
||||||
|
const password = 'StageSr12345!'
|
||||||
|
let r = await api.post('/api/auth/signup', { email, password, organizationName: `SR ${suffix} ${TS}`, phone, plan: 'start' })
|
||||||
|
for (let i = 0; i < 5 && r.status === 429; i++) {
|
||||||
|
await new Promise(res => setTimeout(res, 15000))
|
||||||
|
r = await api.post('/api/auth/signup', { email, password, organizationName: `SR ${suffix} ${TS}`, phone, plan: 'start' })
|
||||||
|
}
|
||||||
|
if (r.status !== 200) throw new Error(`signup ${suffix}: ${r.status} ${JSON.stringify(r.data)}`)
|
||||||
|
const sess = await login(email, password)
|
||||||
|
const auth = makeClient(sess.accessToken)
|
||||||
|
const [stores, units, groups, prices, currencies] = await Promise.all([
|
||||||
|
auth.get('/api/catalog/stores'),
|
||||||
|
auth.get('/api/catalog/units-of-measure'),
|
||||||
|
auth.get('/api/catalog/product-groups'),
|
||||||
|
auth.get('/api/catalog/price-types'),
|
||||||
|
auth.get('/api/catalog/currencies'),
|
||||||
|
])
|
||||||
|
const storeId = stores.data.items.find((s: { isMain: boolean }) => s.isMain).id
|
||||||
|
const unitKgId = units.data.items.find((u: { code: string }) => u.code === '166').id
|
||||||
|
const groupId = groups.data.items.find((g: { parentId: string | null }) => g.parentId == null).id
|
||||||
|
const priceTypeRetailId = prices.data.items.find((p: { isRequired: boolean }) => p.isRequired).id
|
||||||
|
const currencyId = currencies.data.items.find((c: { code: string }) => c.code === 'KZT').id
|
||||||
|
|
||||||
|
const supplier = await auth.post('/api/catalog/counterparties', {
|
||||||
|
name: `ТОО Поставщик ${suffix} ${TS}`, legalName: null, type: 1,
|
||||||
|
bin: `12345600000${bcIdx % 10}`, iin: null, taxNumber: null,
|
||||||
|
countryId: null, address: null, phone: '+77011112233', email: null,
|
||||||
|
bankName: null, bankAccount: null, bik: null, contactPerson: null, notes: null,
|
||||||
|
})
|
||||||
|
if (supplier.status !== 201) throw new Error(`supplier ${suffix}: ${supplier.status} ${JSON.stringify(supplier.data)}`)
|
||||||
|
|
||||||
|
const prod = await auth.post('/api/catalog/products', {
|
||||||
|
name: `SR Prod ${suffix} ${TS}`, article: `SR-${suffix}-${TS}`,
|
||||||
|
unitOfMeasureId: unitKgId, vat: 0, vatEnabled: false, productGroupId: groupId,
|
||||||
|
barcodes: [{ code: generateEan13(bcIdx), type: 1, isPrimary: true }],
|
||||||
|
prices: [{ priceTypeId: priceTypeRetailId, amount: 100, currencyId }],
|
||||||
|
})
|
||||||
|
if (prod.status !== 201) throw new Error(`prod ${suffix}: ${prod.status} ${JSON.stringify(prod.data)}`)
|
||||||
|
|
||||||
|
// Создаём + проводим Supply qty=50
|
||||||
|
const supply = await auth.post('/api/purchases/supplies', {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
supplierId: supplier.data.id, storeId, currencyId,
|
||||||
|
notes: 'initial supply',
|
||||||
|
lines: [{ productId: prod.data.id, quantity: 50, unitPrice: 60 }],
|
||||||
|
})
|
||||||
|
if (supply.status !== 201) throw new Error(`supply ${suffix}: ${supply.status} ${JSON.stringify(supply.data)}`)
|
||||||
|
const supplyPost = await auth.post(`/api/purchases/supplies/${supply.data.id}/post`, {})
|
||||||
|
if (supplyPost.status !== 204) throw new Error(`supply post ${suffix}: ${supplyPost.status} ${JSON.stringify(supplyPost.data)}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
orgId: r.data.organizationId, email, password, token: sess.accessToken,
|
||||||
|
storeId, productId: prod.data.id,
|
||||||
|
supplierId: supplier.data.id, supplyId: supply.data.id,
|
||||||
|
currencyId, unitKgId, groupId, priceTypeRetailId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStock(token: string, storeId: string, productId: string): Promise<number> {
|
||||||
|
const r = await makeClient(token).get(`/api/inventory/stock?storeId=${storeId}`)
|
||||||
|
const row = (r.data?.items ?? []).find((x: { productId: string }) => x.productId === productId)
|
||||||
|
return row?.quantity ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr01_setup({ ctx, step, report }: StepCtx) {
|
||||||
|
ctx.ts = TS
|
||||||
|
ctx.orgA = await bootstrapOrg('a', '+77011160001', 61)
|
||||||
|
await new Promise(r => setTimeout(r, 1500))
|
||||||
|
ctx.orgB = await bootstrapOrg('b', '+77011160002', 62)
|
||||||
|
const stock = await getStock(ctx.orgA.token, ctx.orgA.storeId, ctx.orgA.productId)
|
||||||
|
check(step, { kind: 'api', description: 'Initial Stock=50 у org A', ok: stock === 50, detail: `${stock}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr02_create_draft({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
const res = await api.post('/api/purchases/supplier-returns', {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
supplierId: a.supplierId, storeId: a.storeId, currencyId: a.currencyId,
|
||||||
|
referenceSupplyId: a.supplyId, notes: 'возврат брака',
|
||||||
|
lines: [{ productId: a.productId, quantity: 5, unitPrice: 60 }],
|
||||||
|
})
|
||||||
|
check(step, { kind: 'api', description: 'POST supplier-returns → 201', ok: res.status === 201, detail: `${res.status} ${asString(res.data).slice(0, 200)}` })
|
||||||
|
if (res.status !== 201) return
|
||||||
|
ctx.returnIdA = res.data.id
|
||||||
|
check(step, { kind: 'api', description: 'Total = 5*60 = 300', ok: res.data.total === 300, detail: `total=${res.data.total}` })
|
||||||
|
check(step, { kind: 'api', description: 'Status = Draft (0)', ok: res.data.status === 0, detail: `status=${res.data.status}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr03_update_draft({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA || !ctx.returnIdA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
const upd = await api.put(`/api/purchases/supplier-returns/${ctx.returnIdA}`, {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
supplierId: a.supplierId, storeId: a.storeId, currencyId: a.currencyId,
|
||||||
|
referenceSupplyId: a.supplyId, notes: 'updated',
|
||||||
|
lines: [{ productId: a.productId, quantity: 10, unitPrice: 60 }],
|
||||||
|
})
|
||||||
|
check(step, { kind: 'api', description: 'PUT supplier-return → 204 (фикс EF8)', ok: upd.status === 204, detail: `${upd.status} ${asString(upd.data).slice(0, 200)}` })
|
||||||
|
const got = await api.get(`/api/purchases/supplier-returns/${ctx.returnIdA}`)
|
||||||
|
check(step, { kind: 'api', description: 'После PUT: total=600', ok: got.data?.total === 600, detail: `total=${got.data?.total}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr04_post_stock_down({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA || !ctx.returnIdA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
const before = await getStock(a.token, a.storeId, a.productId)
|
||||||
|
const post = await api.post(`/api/purchases/supplier-returns/${ctx.returnIdA}/post`, {})
|
||||||
|
check(step, { kind: 'api', description: 'POST /post → 204', ok: post.status === 204, detail: `${post.status} ${asString(post.data).slice(0, 200)}` })
|
||||||
|
const after = await getStock(a.token, a.storeId, a.productId)
|
||||||
|
check(step, { kind: 'api', description: 'Stock -= 10 (50→40)', ok: before - after === 10, detail: `${before} → ${after}` })
|
||||||
|
const movs = await api.get('/api/inventory/movements?take=200')
|
||||||
|
const ours = (movs.data?.items ?? []).filter((m: { documentId: string }) => m.documentId === ctx.returnIdA)
|
||||||
|
check(step, {
|
||||||
|
kind: 'api', description: 'StockMovement type=SupplierReturn создан',
|
||||||
|
ok: ours.length >= 1 && ours[0].type === 'SupplierReturn',
|
||||||
|
detail: `count=${ours.length} type=${ours[0]?.type}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr05_post_more_than_stock({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
const avail = await getStock(a.token, a.storeId, a.productId)
|
||||||
|
const create = await api.post('/api/purchases/supplier-returns', {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
supplierId: a.supplierId, storeId: a.storeId, currencyId: a.currencyId,
|
||||||
|
referenceSupplyId: a.supplyId, notes: 'too much',
|
||||||
|
lines: [{ productId: a.productId, quantity: avail + 100, unitPrice: 60 }],
|
||||||
|
})
|
||||||
|
if (create.status !== 201) {
|
||||||
|
step.notes.push(`create unexpected: ${create.status} ${asString(create.data).slice(0, 200)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const post = await api.post(`/api/purchases/supplier-returns/${create.data.id}/post`, {})
|
||||||
|
check(step, {
|
||||||
|
kind: 'api', description: 'Возврат > остатка → 409',
|
||||||
|
ok: post.status === 409 && /больше|есть на складе|минус|нал/i.test(asString(post.data)),
|
||||||
|
detail: `${post.status} ${asString(post.data).slice(0, 200)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr06_reference_supply_validation({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
|
||||||
|
// Создадим второго поставщика и попробуем привязать ReferenceSupplyId от первого
|
||||||
|
// → должна быть валидация что supplier совпадает с supplier из исходной приёмки.
|
||||||
|
const other = await api.post('/api/catalog/counterparties', {
|
||||||
|
name: `Other Supp ${TS}`, legalName: null, type: 1,
|
||||||
|
bin: '999999000088', iin: null, taxNumber: null, countryId: null,
|
||||||
|
address: null, phone: null, email: null, bankName: null, bankAccount: null,
|
||||||
|
bik: null, contactPerson: null, notes: null,
|
||||||
|
})
|
||||||
|
if (other.status !== 201) {
|
||||||
|
step.notes.push(`other supplier create: ${other.status} ${asString(other.data).slice(0, 200)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.otherSupplierIdA = other.data.id
|
||||||
|
|
||||||
|
const mismatch = await api.post('/api/purchases/supplier-returns', {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
supplierId: ctx.otherSupplierIdA, storeId: a.storeId, currencyId: a.currencyId,
|
||||||
|
referenceSupplyId: a.supplyId, notes: 'wrong supplier',
|
||||||
|
lines: [{ productId: a.productId, quantity: 1, unitPrice: 60 }],
|
||||||
|
})
|
||||||
|
check(step, {
|
||||||
|
kind: 'api', description: 'ReferenceSupplyId с другим supplier → 400',
|
||||||
|
ok: mismatch.status === 400 && /совпад|supplier/i.test(asString(mismatch.data)),
|
||||||
|
detail: `${mismatch.status} ${asString(mismatch.data).slice(0, 200)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr07_unpost({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA || !ctx.returnIdA) { step.status = 'skip'; return }
|
||||||
|
const a = ctx.orgA
|
||||||
|
const api = makeClient(a.token)
|
||||||
|
const before = await getStock(a.token, a.storeId, a.productId)
|
||||||
|
const unpost = await api.post(`/api/purchases/supplier-returns/${ctx.returnIdA}/unpost`, {})
|
||||||
|
check(step, { kind: 'api', description: 'POST /unpost → 204', ok: unpost.status === 204, detail: `${unpost.status}` })
|
||||||
|
const after = await getStock(a.token, a.storeId, a.productId)
|
||||||
|
check(step, { kind: 'api', description: 'Stock += 10 обратно', ok: after - before === 10, detail: `${before} → ${after}` })
|
||||||
|
const got = await api.get(`/api/purchases/supplier-returns/${ctx.returnIdA}`)
|
||||||
|
check(step, { kind: 'api', description: 'Status = Draft (0)', ok: got.data?.status === 0, detail: `status=${got.data?.status}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function sr08_multi_tenant_isolation({ ctx, step, report }: StepCtx) {
|
||||||
|
if (!ctx.orgA || !ctx.orgB || !ctx.returnIdA) { step.status = 'skip'; return }
|
||||||
|
const apiB = makeClient(ctx.orgB.token)
|
||||||
|
const list = await apiB.get('/api/purchases/supplier-returns?pageSize=200')
|
||||||
|
const sees = (list.data?.items ?? []).some((x: { id: string }) => x.id === ctx.returnIdA)
|
||||||
|
check(step, { kind: 'api', description: 'Org B не видит supplier-return org A', ok: !sees, detail: `total=${list.data?.total} sees=${sees}` })
|
||||||
|
if (sees) report.bug({ step: 'sr08', severity: 'critical', title: 'P0: supplier-return org A виден из B', detail: `id=${ctx.returnIdA}` })
|
||||||
|
const get = await apiB.get(`/api/purchases/supplier-returns/${ctx.returnIdA}`)
|
||||||
|
check(step, { kind: 'api', description: 'GET чужого id → 404', ok: get.status === 404, detail: `${get.status}` })
|
||||||
|
const post = await apiB.post(`/api/purchases/supplier-returns/${ctx.returnIdA}/post`, {})
|
||||||
|
check(step, { kind: 'api', description: 'POST /post чужого → 404', ok: post.status === 404, detail: `${post.status}` })
|
||||||
|
}
|
||||||
28
tests/e2e/scenarios/stage-supplier-return.yml
Normal file
28
tests/e2e/scenarios/stage-supplier-return.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: stage-supplier-return
|
||||||
|
description: |
|
||||||
|
SupplierReturn (возврат поставщику) на test.admin.food-market.kz.
|
||||||
|
Создание Draft, проведение → Stock минус с MovementType.SupplierReturn,
|
||||||
|
Unpost, валидация по референсной приёмке (того же supplier'a),
|
||||||
|
защита от ухода в минус, multi-tenant.
|
||||||
|
|
||||||
|
preconditions:
|
||||||
|
reset_db: false
|
||||||
|
smoke_login_super_admin: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: sr01_setup
|
||||||
|
title: 2 org. У orgA — supplier (Legal), продукт, проведённый Supply qty=50 → Stock=50
|
||||||
|
- id: sr02_create_draft
|
||||||
|
title: POST /api/purchases/supplier-returns Draft → 201; total корректный
|
||||||
|
- id: sr03_update_draft
|
||||||
|
title: PUT — заменить строки (фикс EF8 ExecuteDelete+DbSet.Add) → 204
|
||||||
|
- id: sr04_post_stock_down
|
||||||
|
title: POST /post → 204; Stock -= qty; StockMovement type=SupplierReturn
|
||||||
|
- id: sr05_post_more_than_stock
|
||||||
|
title: Возврат больше stock → 409 «Нельзя вернуть больше, чем есть на складе»
|
||||||
|
- id: sr06_reference_supply_validation
|
||||||
|
title: ReferenceSupplyId должен быть проведён, supplier должен совпадать → 400 при несовпадении
|
||||||
|
- id: sr07_unpost
|
||||||
|
title: POST /unpost → 204; Stock += обратно
|
||||||
|
- id: sr08_multi_tenant_isolation
|
||||||
|
title: Org B не видит supplier-return org A; POST /post чужого → 404
|
||||||
Loading…
Reference in a new issue