@things-factory/sales 10.1.234 → 10.1.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,7 @@ tslib_1.__decorate([
37
37
  ], OpportunityEvent.prototype, "opportunityId", void 0);
38
38
  tslib_1.__decorate([
39
39
  (0, typeorm_1.Column)(),
40
- (0, type_graphql_1.Field)({ description: 'What happened: CREATED, UPDATED, STAGED, WON or LOST' }),
40
+ (0, type_graphql_1.Field)({ description: 'What happened: CREATED, UPDATED, STAGED, WON, LOST, or REOPENED when the record a win became was undone' }),
41
41
  tslib_1.__metadata("design:type", String)
42
42
  ], OpportunityEvent.prototype, "action", void 0);
43
43
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity-event.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-event.ts"],"names":[],"mappings":"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","sourcesContent":["import { User } from '@things-factory/auth-base'\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\n\nimport { Opportunity } from './opportunity'\nimport { OpportunityStage } from './opportunity-rules'\n\n/** The same shape as QuotationEvent, with the stage it left and the stage and probability it took (ADR-0138 decision 6). */\n@Entity()\n@Index('ix_opportunity_event_0', (event: OpportunityEvent) => [event.opportunity, event.createdAt])\n@ObjectType({ description: 'Entity for OpportunityEvent — what happened to an opportunity, with the stage it moved from and to' })\nexport class OpportunityEvent {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((event: OpportunityEvent) => event.domain)\n domainId?: string\n\n @ManyToOne(type => Opportunity, { nullable: false })\n opportunity: Opportunity\n\n @RelationId((event: OpportunityEvent) => event.opportunity)\n @Field({ description: 'The opportunity the event happened to' })\n opportunityId: string\n\n @Column()\n @Field({ description: 'What happened: CREATED, UPDATED, STAGED, WON or LOST' })\n action: string\n\n @Column({ nullable: true })\n @Field(type => OpportunityStage, { nullable: true, description: 'The stage it moved from; empty when it was created' })\n fromStage?: OpportunityStage\n\n @Column()\n @Field(type => OpportunityStage, { description: 'The stage it stood at after the event' })\n stage: OpportunityStage\n\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'The probability it had after the event, in percent' })\n probability: number\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Why, when it was lost' })\n reason?: string\n\n @Column({ type: 'simple-json' })\n @Field(type => ScalarObject, { description: 'The opportunity as it stood after the event' })\n snapshot: any\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n actor?: User\n\n @RelationId((event: OpportunityEvent) => event.actor)\n actorId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When it happened' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'When the row last changed' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'When the row was deleted' })\n deletedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((event: OpportunityEvent) => event.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((event: OpportunityEvent) => event.updater)\n updaterId?: string\n}\n"]}
1
+ {"version":3,"file":"opportunity-event.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-event.ts"],"names":[],"mappings":"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","sourcesContent":["import { User } from '@things-factory/auth-base'\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\n\nimport { Opportunity } from './opportunity'\nimport { OpportunityStage } from './opportunity-rules'\n\n/** The same shape as QuotationEvent, with the stage it left and the stage and probability it took (ADR-0138 decision 6). */\n@Entity()\n@Index('ix_opportunity_event_0', (event: OpportunityEvent) => [event.opportunity, event.createdAt])\n@ObjectType({ description: 'Entity for OpportunityEvent — what happened to an opportunity, with the stage it moved from and to' })\nexport class OpportunityEvent {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((event: OpportunityEvent) => event.domain)\n domainId?: string\n\n @ManyToOne(type => Opportunity, { nullable: false })\n opportunity: Opportunity\n\n @RelationId((event: OpportunityEvent) => event.opportunity)\n @Field({ description: 'The opportunity the event happened to' })\n opportunityId: string\n\n @Column()\n @Field({ description: 'What happened: CREATED, UPDATED, STAGED, WON, LOST, or REOPENED when the record a win became was undone' })\n action: string\n\n @Column({ nullable: true })\n @Field(type => OpportunityStage, { nullable: true, description: 'The stage it moved from; empty when it was created' })\n fromStage?: OpportunityStage\n\n @Column()\n @Field(type => OpportunityStage, { description: 'The stage it stood at after the event' })\n stage: OpportunityStage\n\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'The probability it had after the event, in percent' })\n probability: number\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Why, when it was lost' })\n reason?: string\n\n @Column({ type: 'simple-json' })\n @Field(type => ScalarObject, { description: 'The opportunity as it stood after the event' })\n snapshot: any\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n actor?: User\n\n @RelationId((event: OpportunityEvent) => event.actor)\n actorId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When it happened' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'When the row last changed' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'When the row was deleted' })\n deletedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((event: OpportunityEvent) => event.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((event: OpportunityEvent) => event.updater)\n updaterId?: string\n}\n"]}
@@ -17,3 +17,9 @@ export declare function closeOpportunity(tx: EntityManager, domain: Domain, user
17
17
  closedOn?: string;
18
18
  reason?: string;
19
19
  }): Promise<Opportunity>;
20
+ /**
21
+ * A won opportunity opened again, because the record its win became was undone - an application's contract link taken
22
+ * back (the user, 2026-10-30: 「계약 연결을 취소할 수 있어야지」). Only the application that closed it as won undoes it; a
23
+ * person does not move a won one. It goes back to the open stage it was won from, with that stage's probability.
24
+ */
25
+ export declare function reopenWonOpportunity(tx: EntityManager, domain: Domain, user: any, id: string, reason: string): Promise<Opportunity>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.opportunityInDomain = opportunityInDomain;
4
4
  exports.saveOpportunity = saveOpportunity;
5
5
  exports.closeOpportunity = closeOpportunity;
6
+ exports.reopenWonOpportunity = reopenWonOpportunity;
6
7
  const auth_base_1 = require("@things-factory/auth-base");
7
8
  const biz_base_1 = require("@things-factory/biz-base");
8
9
  const invoice_1 = require("@things-factory/invoice");
@@ -96,6 +97,24 @@ async function closeOpportunity(tx, domain, user, id, outcome, { closedOn, reaso
96
97
  await recordOpportunityEvent(tx, domain, user, closed, outcome, from, outcome === opportunity_rules_1.OpportunityStage.LOST ? closed.lostReason : undefined);
97
98
  return closed;
98
99
  }
100
+ /**
101
+ * A won opportunity opened again, because the record its win became was undone - an application's contract link taken
102
+ * back (the user, 2026-10-30: 「계약 연결을 취소할 수 있어야지」). Only the application that closed it as won undoes it; a
103
+ * person does not move a won one. It goes back to the open stage it was won from, with that stage's probability.
104
+ */
105
+ async function reopenWonOpportunity(tx, domain, user, id, reason) {
106
+ const opportunity = await opportunityInDomain(tx, domain, id);
107
+ if (opportunity.stage !== opportunity_rules_1.OpportunityStage.WON)
108
+ throw new shell_1.Refusal('opportunity-not-won', {}, 'Only a won opportunity is opened again this way');
109
+ if (!reason?.trim())
110
+ throw new shell_1.Refusal('opportunity-reopen-reason-required', {}, 'Say why it is opened again');
111
+ const won = await tx.getRepository(opportunity_event_1.OpportunityEvent).findOne({ where: { opportunity: { id }, action: opportunity_rules_1.OpportunityStage.WON }, order: { createdAt: 'DESC' } });
112
+ const stage = won?.fromStage && (0, opportunity_rules_1.isOpen)(won.fromStage) ? won.fromStage : opportunity_rules_1.OpportunityStage.NEGOTIATION;
113
+ await tx.getRepository(opportunity_1.Opportunity).update({ id }, { stage, probability: (0, opportunity_rules_1.probabilityAfter)(stage), closedOn: null, lostReason: null, updater: user });
114
+ const reopened = await opportunityInDomain(tx, domain, id);
115
+ await recordOpportunityEvent(tx, domain, user, reopened, 'REOPENED', opportunity_rules_1.OpportunityStage.WON, reason.trim());
116
+ return reopened;
117
+ }
99
118
  function recordOpportunityEvent(tx, domain, user, opportunity, action, fromStage, reason) {
100
119
  return (0, sales_event_1.recordSalesEvent)(tx, opportunity_event_1.OpportunityEvent, { opportunity, fromStage: fromStage ?? null, stage: opportunity.stage, probability: opportunity.probability }, { domain, user, action, reason, snapshot: snapshotOf(opportunity) });
101
120
  }
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity-service.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDomainAssignees } from '@things-factory/auth-base'\nimport { Counterparty } from '@things-factory/biz-base'\nimport { currencyDigits, validDay } from '@things-factory/invoice'\nimport { Domain, Refusal, calendarDay } from '@things-factory/shell'\nimport { EntityManager } from 'typeorm'\n\nimport { recordSalesEvent } from '../sales-event'\nimport { Opportunity } from './opportunity'\nimport { OpportunityEvent } from './opportunity-event'\nimport { isOpen, OpportunityStage, probabilityAfter, stageMove, validMonth, validProbability } from './opportunity-rules'\nimport { NewOpportunity } from './opportunity-type'\n\nexport async function opportunityInDomain(tx: EntityManager, domain: Domain, id: string) {\n const opportunity = await tx.getRepository(Opportunity).findOne({ where: { id, domain: { id: domain.id } } })\n if (!opportunity) throw new Refusal('opportunity-not-found', {}, 'Opportunity not found')\n return opportunity\n}\n\n/**\n * Create an opportunity, or change one a person may still change. An open one takes any open stage; a lost one is\n * opened again by giving it an open stage. Closing is `closeOpportunity`'s.\n */\nexport async function saveOpportunity(tx: EntityManager, domain: Domain, user: any, input: NewOpportunity, id?: string) {\n const old = id ? await opportunityInDomain(tx, domain, id) : undefined\n const stage = input.stage ?? old?.stage ?? OpportunityStage.LEAD\n /* A closed one changes only by being opened again, and only a lost one may be. */\n if (old && !isOpen(old.stage) && !(isOpen(stage) && stageMove(old.stage, stage))) throw new Refusal('opportunity-closed', {}, 'A closed opportunity is not changed')\n if (!isOpen(stage)) throw new Refusal('opportunity-stage-open-required', {}, 'Give an open stage; won and lost are closings')\n\n const name = input.name?.trim()\n if (!name) throw new Refusal('opportunity-name-required', {}, 'Name the opportunity')\n if (!Number.isFinite(input.amount) || input.amount < 0) throw new Refusal('opportunity-amount-invalid', {}, 'The expected amount is zero or more')\n const currency = (input.currency?.trim() || 'KRW').toUpperCase()\n currencyDigits(currency)\n if (!validMonth(input.expectedCloseMonth)) throw new Refusal('opportunity-close-month-invalid', {}, 'Give the expected month as YYYY-MM')\n if (input.probability !== undefined && input.probability !== null && !validProbability(input.probability))\n throw new Refusal('opportunity-probability-invalid', { value: String(input.probability), min: 0, max: 100 }, 'The probability is a whole number from 0 to 100')\n\n const counterparties = tx.getRepository(Counterparty)\n const counterparty = input.counterpartyId ? await counterparties.findOne({ where: { id: input.counterpartyId, domain: { id: domain.id } } }) : null\n if (!counterparty) throw new Refusal('opportunity-counterparty-required', {}, 'Choose the counterparty of this domain')\n const endCustomer = input.endCustomerId ? await counterparties.findOne({ where: { id: input.endCustomerId, domain: { id: domain.id } } }) : null\n if (input.endCustomerId && !endCustomer) throw new Refusal('opportunity-end-customer-invalid', {}, 'Choose the end customer among the counterparties of this domain')\n\n const ownerId = input.ownerId || old?.ownerId || user.id\n /* The people work here may be given to — auth-base's one answer, the same a license's recipient gets. */\n if (ownerId !== user.id && ownerId !== old?.ownerId && !(await getDomainAssignees(domain, tx)).some(one => one.id === ownerId))\n throw new Refusal('opportunity-owner-invalid', {}, 'Put it in the charge of a member of this domain')\n\n const staged = !!old && old.stage !== stage\n /* The probability follows the stage unless the person gave one; with the stage unchanged it stays as it was. */\n const probability = !old || staged ? probabilityAfter(stage, input.probability) : input.probability ?? old.probability\n\n const repository = tx.getRepository(Opportunity)\n const row = await repository.save(\n repository.create({\n ...(old ? { id: old.id, creator: old.creatorId ? ({ id: old.creatorId } as any) : undefined } : { creator: user }),\n domain,\n name,\n counterparty,\n endCustomer: endCustomer ?? null,\n amount: input.amount,\n currency,\n expectedCloseMonth: input.expectedCloseMonth,\n stage,\n probability,\n owner: { id: ownerId } as any,\n /* A lost opportunity opened again is no longer closed. */\n closedOn: null,\n lostReason: null,\n description: input.description?.trim() || null,\n updater: user\n })\n )\n const saved = await opportunityInDomain(tx, domain, row.id)\n await recordOpportunityEvent(tx, domain, user, saved, !old ? 'CREATED' : staged ? 'STAGED' : 'UPDATED', staged ? old.stage : undefined)\n return saved\n}\n\n/**\n * Close an open opportunity — lost, with the reason, by a person; won by the application that turned it into its own\n * record (Office's contract, ADR-0138 decision 8). The day is the domain's today unless one is given.\n */\nexport async function closeOpportunity(\n tx: EntityManager,\n domain: Domain,\n user: any,\n id: string,\n outcome: OpportunityStage.WON | OpportunityStage.LOST,\n { closedOn, reason }: { closedOn?: string; reason?: string } = {}\n) {\n const opportunity = await opportunityInDomain(tx, domain, id)\n if (!stageMove(opportunity.stage, outcome)) throw new Refusal('opportunity-stage-move-invalid', {}, 'The opportunity cannot move to that stage')\n if (outcome === OpportunityStage.LOST && !reason?.trim()) throw new Refusal('opportunity-reason-required', {}, 'Say why it was lost')\n const day = closedOn || calendarDay(domain.timezone)\n if (!validDay(day)) throw new Refusal('opportunity-closed-on-invalid', {}, 'Give the closing day as YYYY-MM-DD')\n\n const from = opportunity.stage\n await tx.getRepository(Opportunity).update(\n { id: opportunity.id },\n { stage: outcome, probability: probabilityAfter(outcome), closedOn: day, lostReason: outcome === OpportunityStage.LOST ? reason!.trim() : null, updater: user }\n )\n const closed = await opportunityInDomain(tx, domain, id)\n await recordOpportunityEvent(tx, domain, user, closed, outcome, from, outcome === OpportunityStage.LOST ? closed.lostReason : undefined)\n return closed\n}\n\nfunction recordOpportunityEvent(tx: EntityManager, domain: Domain, user: any, opportunity: Opportunity, action: string, fromStage?: OpportunityStage, reason?: string) {\n return recordSalesEvent(\n tx,\n OpportunityEvent,\n { opportunity, fromStage: fromStage ?? null, stage: opportunity.stage, probability: opportunity.probability },\n { domain, user, action, reason, snapshot: snapshotOf(opportunity) }\n )\n}\n\n/** The opportunity as it stood. */\nfunction snapshotOf(opportunity: Opportunity) {\n const keys = ['id', 'name', 'counterpartyId', 'endCustomerId', 'amount', 'currency', 'expectedCloseMonth', 'stage', 'probability', 'ownerId', 'closedOn', 'lostReason', 'description']\n return Object.fromEntries(keys.map(key => [key, opportunity[key] ?? null]))\n}\n"]}
1
+ {"version":3,"file":"opportunity-service.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDomainAssignees } from '@things-factory/auth-base'\nimport { Counterparty } from '@things-factory/biz-base'\nimport { currencyDigits, validDay } from '@things-factory/invoice'\nimport { Domain, Refusal, calendarDay } from '@things-factory/shell'\nimport { EntityManager } from 'typeorm'\n\nimport { recordSalesEvent } from '../sales-event'\nimport { Opportunity } from './opportunity'\nimport { OpportunityEvent } from './opportunity-event'\nimport { isOpen, OpportunityStage, probabilityAfter, stageMove, validMonth, validProbability } from './opportunity-rules'\nimport { NewOpportunity } from './opportunity-type'\n\nexport async function opportunityInDomain(tx: EntityManager, domain: Domain, id: string) {\n const opportunity = await tx.getRepository(Opportunity).findOne({ where: { id, domain: { id: domain.id } } })\n if (!opportunity) throw new Refusal('opportunity-not-found', {}, 'Opportunity not found')\n return opportunity\n}\n\n/**\n * Create an opportunity, or change one a person may still change. An open one takes any open stage; a lost one is\n * opened again by giving it an open stage. Closing is `closeOpportunity`'s.\n */\nexport async function saveOpportunity(tx: EntityManager, domain: Domain, user: any, input: NewOpportunity, id?: string) {\n const old = id ? await opportunityInDomain(tx, domain, id) : undefined\n const stage = input.stage ?? old?.stage ?? OpportunityStage.LEAD\n /* A closed one changes only by being opened again, and only a lost one may be. */\n if (old && !isOpen(old.stage) && !(isOpen(stage) && stageMove(old.stage, stage))) throw new Refusal('opportunity-closed', {}, 'A closed opportunity is not changed')\n if (!isOpen(stage)) throw new Refusal('opportunity-stage-open-required', {}, 'Give an open stage; won and lost are closings')\n\n const name = input.name?.trim()\n if (!name) throw new Refusal('opportunity-name-required', {}, 'Name the opportunity')\n if (!Number.isFinite(input.amount) || input.amount < 0) throw new Refusal('opportunity-amount-invalid', {}, 'The expected amount is zero or more')\n const currency = (input.currency?.trim() || 'KRW').toUpperCase()\n currencyDigits(currency)\n if (!validMonth(input.expectedCloseMonth)) throw new Refusal('opportunity-close-month-invalid', {}, 'Give the expected month as YYYY-MM')\n if (input.probability !== undefined && input.probability !== null && !validProbability(input.probability))\n throw new Refusal('opportunity-probability-invalid', { value: String(input.probability), min: 0, max: 100 }, 'The probability is a whole number from 0 to 100')\n\n const counterparties = tx.getRepository(Counterparty)\n const counterparty = input.counterpartyId ? await counterparties.findOne({ where: { id: input.counterpartyId, domain: { id: domain.id } } }) : null\n if (!counterparty) throw new Refusal('opportunity-counterparty-required', {}, 'Choose the counterparty of this domain')\n const endCustomer = input.endCustomerId ? await counterparties.findOne({ where: { id: input.endCustomerId, domain: { id: domain.id } } }) : null\n if (input.endCustomerId && !endCustomer) throw new Refusal('opportunity-end-customer-invalid', {}, 'Choose the end customer among the counterparties of this domain')\n\n const ownerId = input.ownerId || old?.ownerId || user.id\n /* The people work here may be given to — auth-base's one answer, the same a license's recipient gets. */\n if (ownerId !== user.id && ownerId !== old?.ownerId && !(await getDomainAssignees(domain, tx)).some(one => one.id === ownerId))\n throw new Refusal('opportunity-owner-invalid', {}, 'Put it in the charge of a member of this domain')\n\n const staged = !!old && old.stage !== stage\n /* The probability follows the stage unless the person gave one; with the stage unchanged it stays as it was. */\n const probability = !old || staged ? probabilityAfter(stage, input.probability) : input.probability ?? old.probability\n\n const repository = tx.getRepository(Opportunity)\n const row = await repository.save(\n repository.create({\n ...(old ? { id: old.id, creator: old.creatorId ? ({ id: old.creatorId } as any) : undefined } : { creator: user }),\n domain,\n name,\n counterparty,\n endCustomer: endCustomer ?? null,\n amount: input.amount,\n currency,\n expectedCloseMonth: input.expectedCloseMonth,\n stage,\n probability,\n owner: { id: ownerId } as any,\n /* A lost opportunity opened again is no longer closed. */\n closedOn: null,\n lostReason: null,\n description: input.description?.trim() || null,\n updater: user\n })\n )\n const saved = await opportunityInDomain(tx, domain, row.id)\n await recordOpportunityEvent(tx, domain, user, saved, !old ? 'CREATED' : staged ? 'STAGED' : 'UPDATED', staged ? old.stage : undefined)\n return saved\n}\n\n/**\n * Close an open opportunity — lost, with the reason, by a person; won by the application that turned it into its own\n * record (Office's contract, ADR-0138 decision 8). The day is the domain's today unless one is given.\n */\nexport async function closeOpportunity(\n tx: EntityManager,\n domain: Domain,\n user: any,\n id: string,\n outcome: OpportunityStage.WON | OpportunityStage.LOST,\n { closedOn, reason }: { closedOn?: string; reason?: string } = {}\n) {\n const opportunity = await opportunityInDomain(tx, domain, id)\n if (!stageMove(opportunity.stage, outcome)) throw new Refusal('opportunity-stage-move-invalid', {}, 'The opportunity cannot move to that stage')\n if (outcome === OpportunityStage.LOST && !reason?.trim()) throw new Refusal('opportunity-reason-required', {}, 'Say why it was lost')\n const day = closedOn || calendarDay(domain.timezone)\n if (!validDay(day)) throw new Refusal('opportunity-closed-on-invalid', {}, 'Give the closing day as YYYY-MM-DD')\n\n const from = opportunity.stage\n await tx.getRepository(Opportunity).update(\n { id: opportunity.id },\n { stage: outcome, probability: probabilityAfter(outcome), closedOn: day, lostReason: outcome === OpportunityStage.LOST ? reason!.trim() : null, updater: user }\n )\n const closed = await opportunityInDomain(tx, domain, id)\n await recordOpportunityEvent(tx, domain, user, closed, outcome, from, outcome === OpportunityStage.LOST ? closed.lostReason : undefined)\n return closed\n}\n\n/**\n * A won opportunity opened again, because the record its win became was undone - an application's contract link taken\n * back (the user, 2026-10-30: 「계약 연결을 취소할 수 있어야지」). Only the application that closed it as won undoes it; a\n * person does not move a won one. It goes back to the open stage it was won from, with that stage's probability.\n */\nexport async function reopenWonOpportunity(tx: EntityManager, domain: Domain, user: any, id: string, reason: string) {\n const opportunity = await opportunityInDomain(tx, domain, id)\n if (opportunity.stage !== OpportunityStage.WON) throw new Refusal('opportunity-not-won', {}, 'Only a won opportunity is opened again this way')\n if (!reason?.trim()) throw new Refusal('opportunity-reopen-reason-required', {}, 'Say why it is opened again')\n const won = await tx.getRepository(OpportunityEvent).findOne({ where: { opportunity: { id }, action: OpportunityStage.WON } as any, order: { createdAt: 'DESC' } as any })\n const stage = won?.fromStage && isOpen(won.fromStage) ? won.fromStage : OpportunityStage.NEGOTIATION\n await tx.getRepository(Opportunity).update({ id }, { stage, probability: probabilityAfter(stage), closedOn: null, lostReason: null, updater: user } as any)\n const reopened = await opportunityInDomain(tx, domain, id)\n await recordOpportunityEvent(tx, domain, user, reopened, 'REOPENED', OpportunityStage.WON, reason.trim())\n return reopened\n}\n\nfunction recordOpportunityEvent(tx: EntityManager, domain: Domain, user: any, opportunity: Opportunity, action: string, fromStage?: OpportunityStage, reason?: string) {\n return recordSalesEvent(\n tx,\n OpportunityEvent,\n { opportunity, fromStage: fromStage ?? null, stage: opportunity.stage, probability: opportunity.probability },\n { domain, user, action, reason, snapshot: snapshotOf(opportunity) }\n )\n}\n\n/** The opportunity as it stood. */\nfunction snapshotOf(opportunity: Opportunity) {\n const keys = ['id', 'name', 'counterpartyId', 'endCustomerId', 'amount', 'currency', 'expectedCloseMonth', 'stage', 'probability', 'ownerId', 'closedOn', 'lostReason', 'description']\n return Object.fromEntries(keys.map(key => [key, opportunity[key] ?? null]))\n}\n"]}