@things-factory/sales 10.1.233 → 10.1.235
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.
- package/dist-client/pages/opportunity-editor.js +3 -1
- package/dist-client/pages/opportunity-editor.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/opportunity/opportunity-event.js +1 -1
- package/dist-server/service/opportunity/opportunity-event.js.map +1 -1
- package/dist-server/service/opportunity/opportunity-service.d.ts +6 -0
- package/dist-server/service/opportunity/opportunity-service.js +20 -1
- package/dist-server/service/opportunity/opportunity-service.js.map +1 -1
- package/dist-server/service/opportunity/opportunity-type.js +1 -1
- package/dist-server/service/opportunity/opportunity-type.js.map +1 -1
- package/dist-server/service/sales-product/sales-product-service.js +3 -0
- package/dist-server/service/sales-product/sales-product-service.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/translations/en.json +6 -2
- package/translations/es.json +6 -2
- package/translations/ja.json +6 -2
- package/translations/ko.json +6 -2
- package/translations/ms.json +6 -2
- package/translations/zh.json +6 -2
|
@@ -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
|
|
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":";;;;AAAA,yDAAgD;AAChD,iDAA4D;AAC5D,+CAAyD;AACzD,qCAAoJ;AAEpJ,+CAA2C;AAC3C,2DAAsD;AAEtD,4HAA4H;AAIrH,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CA2E5B,CAAA;AA3EY,4CAAgB;AAGlB;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,CAAC;;4CACC;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACjC,cAAM;gDAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,KAAuB,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;;kDACrC;AAGjB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,yBAAW,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;sCACvC,yBAAW;qDAAA;AAIxB;IAFC,IAAA,oBAAU,EAAC,CAAC,KAAuB,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC;IAC1D,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;uDAC3C;AAIrB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,
|
|
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");
|
|
@@ -39,7 +40,7 @@ async function saveOpportunity(tx, domain, user, input, id) {
|
|
|
39
40
|
if (!(0, opportunity_rules_1.validMonth)(input.expectedCloseMonth))
|
|
40
41
|
throw new shell_1.Refusal('opportunity-close-month-invalid', {}, 'Give the expected month as YYYY-MM');
|
|
41
42
|
if (input.probability !== undefined && input.probability !== null && !(0, opportunity_rules_1.validProbability)(input.probability))
|
|
42
|
-
throw new shell_1.Refusal('opportunity-probability-invalid', {}, 'The probability is a whole number from 0 to 100');
|
|
43
|
+
throw new shell_1.Refusal('opportunity-probability-invalid', { value: String(input.probability), min: 0, max: 100 }, 'The probability is a whole number from 0 to 100');
|
|
43
44
|
const counterparties = tx.getRepository(biz_base_1.Counterparty);
|
|
44
45
|
const counterparty = input.counterpartyId ? await counterparties.findOne({ where: { id: input.counterpartyId, domain: { id: domain.id } } }) : null;
|
|
45
46
|
if (!counterparty)
|
|
@@ -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', {}, '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"]}
|
|
@@ -37,7 +37,7 @@ tslib_1.__decorate([
|
|
|
37
37
|
tslib_1.__metadata("design:type", String)
|
|
38
38
|
], NewOpportunity.prototype, "stage", void 0);
|
|
39
39
|
tslib_1.__decorate([
|
|
40
|
-
(0, type_graphql_1.Field)(type => type_graphql_1.
|
|
40
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Float, { nullable: true, description: 'Chance of winning in percent, a whole number from 0 to 100 - another is refused naming it; the stage gives it when empty' }),
|
|
41
41
|
tslib_1.__metadata("design:type", Number)
|
|
42
42
|
], NewOpportunity.prototype, "probability", void 0);
|
|
43
43
|
tslib_1.__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity-type.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAuE;AAEvE,+CAA2C;AAC3C,2DAAsD;AAG/C,IAAM,cAAc,GAApB,MAAM,cAAc;CAW1B,CAAA;AAXY,wCAAc;AACiC;IAAzD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;;4CAAa;AACW;IAAhF,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;;sDAAuB;AACM;IAA5G,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;;qDAAuB;AAC9D;IAApE,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAK,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;8CAAe;AACD;IAAjF,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;;gDAAkB;AAC5B;IAAtE,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,6CAA6C,EAAE,CAAC;;0DAA2B;AACiC;IAAjI,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oCAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;6CAAyB;
|
|
1
|
+
{"version":3,"file":"opportunity-type.js","sourceRoot":"","sources":["../../../server/service/opportunity/opportunity-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAuE;AAEvE,+CAA2C;AAC3C,2DAAsD;AAG/C,IAAM,cAAc,GAApB,MAAM,cAAc;CAW1B,CAAA;AAXY,wCAAc;AACiC;IAAzD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;;4CAAa;AACW;IAAhF,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;;sDAAuB;AACM;IAA5G,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;;qDAAuB;AAC9D;IAApE,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAK,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;8CAAe;AACD;IAAjF,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;;gDAAkB;AAC5B;IAAtE,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,6CAA6C,EAAE,CAAC;;0DAA2B;AACiC;IAAjI,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oCAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;6CAAyB;AACyB;IAAlL,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0HAA0H,EAAE,CAAC;;mDAAqB;AAC1G;IAA5F,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;+CAAiB;AAC5D;IAAhD,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;;mDAAqB;yBAV1D,cAAc;IAD1B,IAAA,wBAAS,EAAC,EAAE,WAAW,EAAE,2DAA2D,EAAE,CAAC;GAC3E,cAAc,CAW1B;AAGM,IAAM,eAAe,GAArB,MAAM,eAAe;CAG3B,CAAA;AAHY,0CAAe;AACkD;IAA3E,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,yBAAW,CAAC,EAAE,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;8CAAqB;AACH;IAA5F,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,sDAAsD,EAAE,CAAC;;8CAAc;0BAF/F,eAAe;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;GAC1C,eAAe,CAG3B;AAGM,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;CAInC,CAAA;AAJY,0DAAuB;AAC6B;IAA9D,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oCAAgB,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;;sDAAwB;AAC+B;IAApH,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8EAA8E,EAAE,CAAC;;4DAAoB;AAC7B;IAA1G,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iFAAiF,EAAE,CAAC;;qDAAc;kCAH7G,uBAAuB;IADnC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yDAAyD,EAAE,CAAC;GAC1E,uBAAuB,CAInC","sourcesContent":["import { Field, Float, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { Opportunity } from './opportunity'\nimport { OpportunityStage } from './opportunity-rules'\n\n@InputType({ description: 'An opportunity to create, or what a person changes on one' })\nexport class NewOpportunity {\n @Field({ description: 'What the opportunity is called' }) name: string\n @Field({ description: 'Id of the counterparty the domain would contract with' }) counterpartyId: string\n @Field({ nullable: true, description: 'Id of the end customer, when the work comes through another party' }) endCustomerId?: string\n @Field(type => Float, { description: 'Expected amount before tax' }) amount: number\n @Field({ nullable: true, description: 'ISO 4217 currency code, KRW when empty' }) currency?: string\n @Field({ description: 'Month the domain expects to win it, YYYY-MM' }) expectedCloseMonth: string\n @Field(type => OpportunityStage, { nullable: true, description: 'An open stage; a new opportunity starts as a lead when empty' }) stage?: OpportunityStage\n @Field(type => Float, { nullable: true, description: 'Chance of winning in percent, a whole number from 0 to 100 - another is refused naming it; the stage gives it when empty' }) probability?: number\n @Field({ nullable: true, description: 'Id of the person in charge; the caller when empty' }) ownerId?: string\n @Field({ nullable: true, description: 'Notes' }) description?: string\n}\n\n@ObjectType({ description: 'A page of opportunities' })\nexport class OpportunityList {\n @Field(type => [Opportunity], { description: 'Opportunities on the page' }) items: Opportunity[]\n @Field(type => Int, { description: 'Number of opportunities matching the list parameters' }) total: number\n}\n\n@ObjectType({ description: 'A stage and the probability it starts an opportunity at' })\nexport class OpportunityStageDefault {\n @Field(type => OpportunityStage, { description: 'The stage' }) stage: OpportunityStage\n @Field(type => Int, { description: 'The probability the stage gives, in percent — a judgement, not a measurement' }) probability: number\n @Field({ description: 'Whether a person moves an opportunity through this stage; won and lost close it' }) open: boolean\n}\n"]}
|
|
@@ -40,6 +40,9 @@ function rowOf(patch, old) {
|
|
|
40
40
|
const price = patch.unitPrice ?? 0;
|
|
41
41
|
if (!Number.isFinite(price) || price < 0)
|
|
42
42
|
throw new shell_1.Refusal('sales-product-price-invalid', {}, 'The price is zero or more');
|
|
43
|
+
/* A quotation line takes a price to four decimals (invoice's document-amounts) - a finer one would be refused there. */
|
|
44
|
+
if (Math.abs(price * 10_000 - Math.round(price * 10_000)) > 1e-6)
|
|
45
|
+
throw new shell_1.Refusal('sales-product-price-decimals', { value: String(price), max: 4 }, 'unitPrice: at most four decimals');
|
|
43
46
|
row.unitPrice = price;
|
|
44
47
|
}
|
|
45
48
|
if (patch.taxKind !== undefined || !old)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sales-product-service.js","sourceRoot":"","sources":["../../../server/service/sales-product/sales-product-service.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"sales-product-service.js","sourceRoot":"","sources":["../../../server/service/sales-product/sales-product-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { currencyDigits } from '@things-factory/invoice'\nimport { Domain, Refusal } from '@things-factory/shell'\nimport { EntityManager, In } from 'typeorm'\n\nimport { SalesProduct } from './sales-product'\nimport { SalesProductKind, unitOf } from './sales-product-rules'\nimport { SalesProductPatch } from './sales-product-type'\n\nconst TEXT = ['grade', 'spec', 'description'] as const\n\n/** A patch as the row it writes: words trimmed, the unit the kind's when blank, the price and currency checked. */\nfunction rowOf(patch: SalesProductPatch, old?: SalesProduct): Partial<SalesProduct> {\n const row: Record<string, any> = {}\n if (patch.code !== undefined || !old) {\n const code = String(patch.code ?? '').trim()\n if (!code) throw new Refusal('sales-product-code-required', {}, 'Give the product a code')\n row.code = code\n }\n if (patch.name !== undefined || !old) {\n const name = String(patch.name ?? '').trim()\n if (!name) throw new Refusal('sales-product-name-required', {}, 'Name the product')\n row.name = name\n }\n const kind = patch.kind ?? old?.kind ?? SalesProductKind.GOODS\n row.kind = kind\n for (const field of TEXT) if (patch[field] !== undefined) row[field] = String(patch[field] ?? '').trim() || null\n /* A blank unit is the kind's - also when the kind changes and the unit was the old kind's own. */\n if (patch.unit !== undefined || !old) row.unit = unitOf(kind, patch.unit)\n else if (patch.kind !== undefined) row.unit = old.unit === unitOf(old.kind) ? unitOf(kind) : old.unit\n if (patch.currency !== undefined || !old) {\n row.currency = (String(patch.currency ?? '').trim() || 'KRW').toUpperCase()\n currencyDigits(row.currency)\n }\n if (patch.unitPrice !== undefined || !old) {\n const price = patch.unitPrice ?? 0\n if (!Number.isFinite(price) || price < 0) throw new Refusal('sales-product-price-invalid', {}, 'The price is zero or more')\n /* A quotation line takes a price to four decimals (invoice's document-amounts) - a finer one would be refused there. */\n if (Math.abs(price * 10_000 - Math.round(price * 10_000)) > 1e-6)\n throw new Refusal('sales-product-price-decimals', { value: String(price), max: 4 }, 'unitPrice: at most four decimals')\n row.unitPrice = price\n }\n if (patch.taxKind !== undefined || !old) row.taxKind = patch.taxKind ?? 'TAXABLE'\n if (patch.active !== undefined || !old) row.active = patch.active !== false\n return row\n}\n\n/**\n * Apply grid rows to a domain's products: `+` (or no id) registers, `M` changes, `-` deletes (softly). A code is the\n * domain's once among the products not deleted. Nothing names a product - a quotation line copies what it says - so a\n * product is deleted freely.\n */\nexport async function saveSalesProducts(tx: EntityManager, domain: Domain, user: any, patches: SalesProductPatch[]) {\n const repository = tx.getRepository(SalesProduct)\n const deletes = patches.filter(one => one.cuFlag === '-' && one.id).map(one => one.id!)\n const changes = patches.filter(one => one.cuFlag !== '-')\n\n if (deletes.length) {\n const owned = await repository.find({ where: { domain: { id: domain.id }, id: In(deletes) }, select: ['id'] })\n if (owned.length !== deletes.length) throw new Refusal('sales-product-not-found', {}, 'A product was not found - reload and try again')\n await repository.softDelete({ id: In(deletes) })\n }\n\n const rows: Partial<SalesProduct>[] = []\n for (const patch of changes) {\n const old = patch.id && patch.cuFlag !== '+' ? await repository.findOne({ where: { domain: { id: domain.id }, id: patch.id } }) : undefined\n if (patch.id && patch.cuFlag !== '+' && !old) throw new Refusal('sales-product-not-found', {}, 'A product was not found - reload and try again')\n rows.push({ ...(old ?? {}), ...rowOf(patch, old ?? undefined) })\n }\n\n /* One code, one product: against the rows kept and against one another in the batch. */\n const codes = rows.map(row => row.code!)\n const taken = codes.length ? await repository.find({ where: { domain: { id: domain.id }, code: In(codes) }, select: ['id', 'code'] }) : []\n for (const [index, row] of rows.entries()) {\n const clash = taken.find(one => one.code === row.code && one.id !== row.id) || rows.find((other, at) => at < index && other.code === row.code)\n if (clash) throw new Refusal('sales-product-code-duplicate', { code: row.code }, `Product code ${row.code} is already in use`)\n }\n\n const saved: SalesProduct[] = []\n for (const row of rows) saved.push(await repository.save({ ...row, domain, ...(row.id ? {} : { creator: user }), updater: user } as SalesProduct))\n return saved\n}\n"]}
|