fix(controller): списывать скоропорт с отступом после истечения срока (post_expiry_grace_days)
Возвращён регулируемый параметр списания, но с корректной семантикой: товар попадает в проект списания, только пролежав N дней ПОСЛЕ истечения срока (expiry_date <= now - grace), default 7. Свежепросроченное ещё может быть забрано получателем — списываем лишь окончательно испорченное. Крон остаётся ежемесячным (каждое 1-е число). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,7 @@ interface IConfig {
|
||||
// Настройки автосписания скоропорта (видимы в форме установки).
|
||||
writeoff: {
|
||||
auto_proposal_enabled: boolean; // Автоформирование проекта списания (по умолчанию вкл.)
|
||||
post_expiry_grace_days: number; // Списывать спустя N дней после истечения срока (по умолчанию 7)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
+10
-7
@@ -22,11 +22,13 @@ import { MARKETPLACE_WRITEOFF_DRAFT_BUILT_EVENT } from '../events/marketplace-no
|
||||
* Раз в месяц проходит по `marketplace_inventory.expiry_date`. Если в
|
||||
* настройках расширения `writeoff.auto_proposal_enabled = true` —
|
||||
* формирует DRAFT-проект списания со всеми позициями, у которых срок
|
||||
* годности УЖЕ истёк (`expiry_date <= now`). Иначе — только эмитит
|
||||
* напоминание председателю, чтобы тот собрал корзину вручную.
|
||||
* годности истёк уже как минимум `writeoff.post_expiry_grace_days` дней
|
||||
* назад (`expiry_date <= now - grace`). Иначе — только эмитит напоминание
|
||||
* председателю, чтобы тот собрал корзину вручную.
|
||||
*
|
||||
* Списываем по факту порчи, а не заранее: в кандидаты попадает только
|
||||
* непригодный товар (срок годности прошёл).
|
||||
* Списываем по факту порчи, а не заранее, и с отступом: свежепросроченное
|
||||
* ещё может быть забрано получателем — в кандидаты идёт только то, что
|
||||
* пролежало просроченным достаточно долго и окончательно испортилось.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MarketplaceWriteoffCronService implements OnModuleInit {
|
||||
@@ -71,6 +73,7 @@ export class MarketplaceWriteoffCronService implements OnModuleInit {
|
||||
const extension = this.extensionDomainService ? await this.extensionDomainService.getAppByName('market') : null;
|
||||
const cfg = extension?.config as IConfig | undefined;
|
||||
const auto = cfg?.writeoff?.auto_proposal_enabled ?? true;
|
||||
const graceDays = cfg?.writeoff?.post_expiry_grace_days ?? 7;
|
||||
|
||||
if (!auto) {
|
||||
this.logger.info(
|
||||
@@ -88,12 +91,12 @@ export class MarketplaceWriteoffCronService implements OnModuleInit {
|
||||
// EXCESS_RETURNED_TO_WAREHOUSE, items «без юр. оформления» старше
|
||||
// threshold writeoff_returned_age_days) — Phase 2: требуется
|
||||
// расширение marketplace_inventory.status + поля returned_at / age_days.
|
||||
const now = new Date();
|
||||
const cutoff = new Date(Date.now() - graceDays * 86_400_000);
|
||||
const candidates = await this.inventoryRepo.find({
|
||||
where: {
|
||||
coopname,
|
||||
status: 'LABELED',
|
||||
expiry_date: LessThanOrEqual(now),
|
||||
expiry_date: LessThanOrEqual(cutoff),
|
||||
},
|
||||
take: 200,
|
||||
order: { expiry_date: 'ASC' },
|
||||
@@ -101,7 +104,7 @@ export class MarketplaceWriteoffCronService implements OnModuleInit {
|
||||
|
||||
if (candidates.length === 0) {
|
||||
this.logger.info(
|
||||
`[WRITEOFF_CRON] не найдено позиций с истёкшим сроком годности (coopname=${coopname})`
|
||||
`[WRITEOFF_CRON] не найдено позиций, просроченных более чем на ${graceDays} дн. (coopname=${coopname})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-1
@@ -70,7 +70,8 @@ export interface MarketplaceInventoryProps {
|
||||
* Story 8.3 (Эпик 8): срок годности позиции. Проставляется при
|
||||
* маркировке как `labeled_at + Offer.warranty_days * 86400`. По нему
|
||||
* крон Эпика 8 формирует DRAFT-проект списания, когда срок годности
|
||||
* истёк (`expiry_date <= now`).
|
||||
* истёк уже более чем `writeoff.post_expiry_grace_days` дней назад
|
||||
* (`expiry_date <= now - grace`).
|
||||
*
|
||||
* Nullable: для исторических записей без warranty_days и для позиций
|
||||
* с бессрочными офферами (`warranty_days = 0`).
|
||||
|
||||
@@ -32,15 +32,19 @@ export interface ICoopAcceptanceConfig {
|
||||
* Story 8.3 (Эпик 8): настройки авто-проекта списания скоропорта.
|
||||
*
|
||||
* `auto_proposal_enabled` — если true, ежемесячный крон собирает позиции с
|
||||
* УЖЕ истёкшим сроком годности (`MarketplaceInventory.expiry_date <= now`) в
|
||||
* DRAFT-проект и шлёт нотификацию председателю на ревью. Если false, крон
|
||||
* только пушит напоминание; председатель формирует корзину вручную.
|
||||
* давно истёкшим сроком годности в DRAFT-проект и шлёт нотификацию
|
||||
* председателю на ревью. Если false, крон только пушит напоминание;
|
||||
* председатель формирует корзину вручную.
|
||||
*
|
||||
* Списываем по факту порчи (срок годности истёк), а не заранее: на списание
|
||||
* попадает только то, что уже непригодно.
|
||||
* `post_expiry_grace_days` — сколько дней товар должен пролежать ПОСЛЕ
|
||||
* истечения срока годности, прежде чем попасть в кандидаты на списание.
|
||||
* Крон отбирает позиции с `expiry_date <= now - post_expiry_grace_days`.
|
||||
* Свежепросроченное ещё может быть забрано получателем — списываем только
|
||||
* окончательно испорченное (пролежавшее лишнее время).
|
||||
*/
|
||||
export interface IWriteoffConfig {
|
||||
auto_proposal_enabled: boolean;
|
||||
post_expiry_grace_days: number;
|
||||
}
|
||||
|
||||
// Конфигурация для расширения marketplace
|
||||
@@ -62,6 +66,7 @@ export const defaultConfig: IConfig = {
|
||||
},
|
||||
writeoff: {
|
||||
auto_proposal_enabled: true,
|
||||
post_expiry_grace_days: 7,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -99,8 +104,21 @@ export const Schema = z.object({
|
||||
note: 'Если включено, раз в месяц собирается проект списания товаров с истёкшим сроком годности и отправляется председателю на ревью. Если выключено — приходит только напоминание.',
|
||||
})
|
||||
),
|
||||
post_expiry_grace_days: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(7)
|
||||
.describe(
|
||||
describeField({
|
||||
label: 'Списывать спустя (после истечения срока)',
|
||||
note: 'Сколько дней товар должен пролежать после истечения срока годности, прежде чем попадёт в проект списания. Свежепросроченное ещё может быть забрано получателем — списываем только окончательно испорченное.',
|
||||
rules: ['val >= 0'],
|
||||
append: 'дн.',
|
||||
})
|
||||
),
|
||||
})
|
||||
.default({ auto_proposal_enabled: true })
|
||||
.default({ auto_proposal_enabled: true, post_expiry_grace_days: 7 })
|
||||
.describe(
|
||||
describeField({
|
||||
label: 'Списание скоропорта',
|
||||
|
||||
Reference in New Issue
Block a user