@managemint-solutions/sdk 0.43.0 → 0.45.0
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/additional-details/index.js +7 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -0
- package/dist/clients/index.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/leads/dto.d.ts +35 -0
- package/dist/leads/dto.js +209 -0
- package/dist/leads/index.d.ts +54 -0
- package/dist/leads/index.js +413 -0
- package/dist/leads/won.d.ts +7 -0
- package/dist/leads/won.js +26 -0
- package/dist/notes/index.js +7 -2
- package/dist/permissions/dto.d.ts +4 -0
- package/dist/permissions/dto.js +20 -0
- package/dist/pricebooks/index.js +5 -3
- package/dist/projects/index.js +4 -2
- package/dist/service-client/index.d.ts +7 -0
- package/dist/service-client/index.js +12 -0
- package/dist/statuses/index.d.ts +8 -1
- package/dist/statuses/index.js +26 -1
- package/dist/supporting-files/index.js +7 -2
- package/dist/tasks/index.js +5 -3
- package/package.json +2 -2
|
@@ -15,9 +15,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.AdditionalDetailsResource = void 0;
|
|
18
|
-
const enum_1 = require("@managemint-solutions/entities/
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/additional-details/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
20
|
const audit_1 = require("../audit");
|
|
20
21
|
const errors_1 = require("../errors");
|
|
22
|
+
const won_1 = require("../leads/won");
|
|
21
23
|
const values_1 = require("./values");
|
|
22
24
|
__exportStar(require("./dto"), exports);
|
|
23
25
|
__exportStar(require("./values"), exports);
|
|
@@ -41,7 +43,7 @@ class AdditionalDetailsResource {
|
|
|
41
43
|
this.auth = auth;
|
|
42
44
|
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
43
45
|
table: 'additional_details',
|
|
44
|
-
entityType:
|
|
46
|
+
entityType: enum_2.AuditTrailEntityType.ADDITIONAL_DETAILS,
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
async getById(additionalDetailsId) {
|
|
@@ -68,6 +70,9 @@ class AdditionalDetailsResource {
|
|
|
68
70
|
return data.map(withCastValue);
|
|
69
71
|
}
|
|
70
72
|
async create(entity, entityId, input) {
|
|
73
|
+
if (entity === enum_1.AdditionalDetailsEntityTypes.LEAD) {
|
|
74
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
|
|
75
|
+
}
|
|
71
76
|
const row = await this.table.insert({
|
|
72
77
|
organization_id: this.auth.organization_id,
|
|
73
78
|
entity_type: entity,
|
package/dist/client.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { NotificationsResource } from './notifications';
|
|
|
9
9
|
import { BillingReadsResource } from './billing';
|
|
10
10
|
import { InvoicesReadsResource } from './billing/invoices';
|
|
11
11
|
import { LabelsResource } from './labels';
|
|
12
|
+
import { LeadsResource } from './leads';
|
|
12
13
|
import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from './leave';
|
|
13
14
|
import { OrganizationsResource } from './organizations';
|
|
14
15
|
import { PermissionsResource } from './permissions';
|
|
@@ -58,6 +59,7 @@ export declare class SupabaseClient {
|
|
|
58
59
|
readonly permissions: PermissionsResource;
|
|
59
60
|
readonly users: UsersResource;
|
|
60
61
|
readonly supportingFiles: SupportingFilesResource;
|
|
62
|
+
readonly leads: LeadsResource;
|
|
61
63
|
readonly clients: ClientsResource;
|
|
62
64
|
readonly projects: ProjectsResource;
|
|
63
65
|
readonly tasks: TasksResource;
|
package/dist/client.js
CHANGED
|
@@ -11,6 +11,7 @@ const notifications_1 = require("./notifications");
|
|
|
11
11
|
const billing_1 = require("./billing");
|
|
12
12
|
const invoices_1 = require("./billing/invoices");
|
|
13
13
|
const labels_1 = require("./labels");
|
|
14
|
+
const leads_1 = require("./leads");
|
|
14
15
|
const leave_1 = require("./leave");
|
|
15
16
|
const organizations_1 = require("./organizations");
|
|
16
17
|
const permissions_1 = require("./permissions");
|
|
@@ -43,6 +44,7 @@ class SupabaseClient {
|
|
|
43
44
|
permissions;
|
|
44
45
|
users;
|
|
45
46
|
supportingFiles;
|
|
47
|
+
leads;
|
|
46
48
|
clients;
|
|
47
49
|
projects;
|
|
48
50
|
tasks;
|
|
@@ -71,6 +73,8 @@ class SupabaseClient {
|
|
|
71
73
|
this.users = new users_1.UsersResource(this.supabase, this.auth, this.audit);
|
|
72
74
|
this.supportingFiles = new supporting_files_1.SupportingFilesResource(this.supabase, this.auth, this.audit);
|
|
73
75
|
this.clients = new clients_1.ClientsResource(this.supabase, this.auth, this.audit);
|
|
76
|
+
// After clients and additional details: a convert writes through both.
|
|
77
|
+
this.leads = new leads_1.LeadsResource(this.supabase, this.auth, this.audit, this.additionalDetails);
|
|
74
78
|
this.projects = new projects_1.ProjectsResource(this.supabase, this.auth, this.audit);
|
|
75
79
|
this.tasks = new tasks_1.TasksResource(this.supabase, this.auth, this.audit);
|
|
76
80
|
this.leaveTypes = new leave_1.LeaveTypesResource(this.supabase, this.auth, this.audit);
|
package/dist/clients/index.js
CHANGED
|
@@ -34,7 +34,8 @@ const CLIENT_LIST_SELECT = `
|
|
|
34
34
|
label,
|
|
35
35
|
is_complete,
|
|
36
36
|
is_fail,
|
|
37
|
-
is_triage
|
|
37
|
+
is_triage,
|
|
38
|
+
is_won
|
|
38
39
|
),
|
|
39
40
|
created_by:user_profiles!created_by (*)
|
|
40
41
|
`;
|
|
@@ -50,7 +51,8 @@ const CLIENT_DETAIL_SELECT = `
|
|
|
50
51
|
colour,
|
|
51
52
|
is_complete,
|
|
52
53
|
is_fail,
|
|
53
|
-
is_triage
|
|
54
|
+
is_triage,
|
|
55
|
+
is_won
|
|
54
56
|
),
|
|
55
57
|
contract_client,
|
|
56
58
|
contracted_hours,
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export { AddUserDto, GetUsersQueryDto, UpdateMyProfileDto, UpdateUserDto, UserId
|
|
|
35
35
|
export type { ClaimOutcome, WebhookEventRow, WebhookEventsTable } from './webhook-events';
|
|
36
36
|
export { GetSupportingFilesDto, SupportingFileIdDto } from './supporting-files';
|
|
37
37
|
export { ClientIdDto, CreateClientDto, GetClientsDto, UpdateClientDto } from './clients';
|
|
38
|
+
export { ConvertLeadDto, CreateLeadDto, GetLeadsDto, LeadIdDto, UpdateLeadDto } from './leads';
|
|
38
39
|
export { CreateProjectDto, GetProjectsDto, ProjectIdDto, UpdateProjectDto } from './projects';
|
|
39
40
|
export { AddTaskLinkDto, CreateTaskDto, GetTasksDto, TaskIdDto, TaskLinkIdDto, UpdateTaskDto } from './tasks';
|
|
40
41
|
export type { CreateLeaveEntitlementInput, CreateLeaveRequestRow, LeaveEntitlementCover, ReviewLeaveRequestRow, UpdateLeaveRequestRow, } from './leave';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// to keep. The row and input types they produce and accept are exported for annotations.
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.AddUserDto = exports.UpdatePermissionsDto = exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateLabelDto = exports.LabelIdDto = exports.GetLabelsDto = exports.AddLabelDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
|
|
11
|
-
exports.
|
|
11
|
+
exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.leavePeriodFor = exports.isWithinPeriod = exports.countWorkingDays = exports.addOneYearMinusDay = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.UpdateTaskDto = exports.TaskLinkIdDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.AddTaskLinkDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateLeadDto = exports.LeadIdDto = exports.GetLeadsDto = exports.CreateLeadDto = exports.ConvertLeadDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = void 0;
|
|
12
|
+
exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = void 0;
|
|
12
13
|
var client_1 = require("./client");
|
|
13
14
|
Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
|
|
14
15
|
Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
|
|
@@ -94,6 +95,12 @@ Object.defineProperty(exports, "ClientIdDto", { enumerable: true, get: function
|
|
|
94
95
|
Object.defineProperty(exports, "CreateClientDto", { enumerable: true, get: function () { return clients_1.CreateClientDto; } });
|
|
95
96
|
Object.defineProperty(exports, "GetClientsDto", { enumerable: true, get: function () { return clients_1.GetClientsDto; } });
|
|
96
97
|
Object.defineProperty(exports, "UpdateClientDto", { enumerable: true, get: function () { return clients_1.UpdateClientDto; } });
|
|
98
|
+
var leads_1 = require("./leads");
|
|
99
|
+
Object.defineProperty(exports, "ConvertLeadDto", { enumerable: true, get: function () { return leads_1.ConvertLeadDto; } });
|
|
100
|
+
Object.defineProperty(exports, "CreateLeadDto", { enumerable: true, get: function () { return leads_1.CreateLeadDto; } });
|
|
101
|
+
Object.defineProperty(exports, "GetLeadsDto", { enumerable: true, get: function () { return leads_1.GetLeadsDto; } });
|
|
102
|
+
Object.defineProperty(exports, "LeadIdDto", { enumerable: true, get: function () { return leads_1.LeadIdDto; } });
|
|
103
|
+
Object.defineProperty(exports, "UpdateLeadDto", { enumerable: true, get: function () { return leads_1.UpdateLeadDto; } });
|
|
97
104
|
var projects_1 = require("./projects");
|
|
98
105
|
Object.defineProperty(exports, "CreateProjectDto", { enumerable: true, get: function () { return projects_1.CreateProjectDto; } });
|
|
99
106
|
Object.defineProperty(exports, "GetProjectsDto", { enumerable: true, get: function () { return projects_1.GetProjectsDto; } });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { UUID } from 'crypto';
|
|
2
|
+
import type { ConvertLeadDto as ConvertLeadDtoType, CreateLeadDto as CreateLeadDtoType, GetLeadsDto as GetLeadsDtoType, LeadIdDto as LeadIdDtoType, UpdateLeadDto as UpdateLeadDtoType } from '@managemint-solutions/entities/leads/dto';
|
|
3
|
+
import { LeadSource } from '@managemint-solutions/entities/leads/enum';
|
|
4
|
+
export declare class GetLeadsDto implements Omit<GetLeadsDtoType, 'status_id_in'> {
|
|
5
|
+
readonly search?: string | null;
|
|
6
|
+
readonly page: number;
|
|
7
|
+
readonly page_size: number;
|
|
8
|
+
readonly created_after?: string | null;
|
|
9
|
+
readonly created_before?: string | null;
|
|
10
|
+
readonly status_id_in?: string[] | null;
|
|
11
|
+
readonly status_id_not_in?: string[] | null;
|
|
12
|
+
readonly assigned_to_id?: string | null;
|
|
13
|
+
readonly completed?: boolean | null;
|
|
14
|
+
readonly archive: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class LeadIdDto implements LeadIdDtoType {
|
|
17
|
+
readonly leadId: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class CreateLeadDto implements CreateLeadDtoType {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly contact_person?: string | null;
|
|
22
|
+
readonly email?: string | null;
|
|
23
|
+
readonly phone?: string | null;
|
|
24
|
+
readonly status_id?: UUID | null;
|
|
25
|
+
readonly assigned_to_id?: string | null;
|
|
26
|
+
readonly source?: LeadSource | null;
|
|
27
|
+
readonly estimated_value_minor?: number | null;
|
|
28
|
+
readonly follow_up_date?: string | null;
|
|
29
|
+
readonly lost_reason?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare class UpdateLeadDto extends CreateLeadDto implements UpdateLeadDtoType {
|
|
32
|
+
}
|
|
33
|
+
export declare class ConvertLeadDto implements ConvertLeadDtoType {
|
|
34
|
+
readonly client_status_id: string | null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ConvertLeadDto = exports.UpdateLeadDto = exports.CreateLeadDto = exports.LeadIdDto = exports.GetLeadsDto = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
14
|
+
const class_transformer_1 = require("class-transformer");
|
|
15
|
+
const enum_1 = require("@managemint-solutions/entities/leads/enum");
|
|
16
|
+
const validators_1 = require("../validators");
|
|
17
|
+
// class-validator versions of the shared DTO types from @managemint-solutions/entities, so the
|
|
18
|
+
// wire contract cannot drift. `status_id` is typed as the entities' `UUID | null` for the same
|
|
19
|
+
// reason; class-validator only sees the runtime string.
|
|
20
|
+
// `status_id_in` is omitted from the implemented type on purpose: the entities type carries the
|
|
21
|
+
// CSV wire value (`string | null`), while this class carries the `@Transform`-parsed
|
|
22
|
+
// `string[] | null` the query builder feeds to `.in('status_id', ...)`. `status_id_not_in` is not
|
|
23
|
+
// in the entities type at all: it is what the api resolves `completed: false` into, as for tasks.
|
|
24
|
+
class GetLeadsDto {
|
|
25
|
+
search;
|
|
26
|
+
page = 1;
|
|
27
|
+
page_size = 10;
|
|
28
|
+
created_after;
|
|
29
|
+
created_before;
|
|
30
|
+
status_id_in;
|
|
31
|
+
status_id_not_in;
|
|
32
|
+
assigned_to_id;
|
|
33
|
+
// Tri-state: true = only won leads, false = only open ones, omitted = no completion filter.
|
|
34
|
+
// The api resolves it to status ids before it reaches `list`.
|
|
35
|
+
completed;
|
|
36
|
+
archive = false;
|
|
37
|
+
}
|
|
38
|
+
exports.GetLeadsDto = GetLeadsDto;
|
|
39
|
+
__decorate([
|
|
40
|
+
(0, class_validator_1.IsOptional)(),
|
|
41
|
+
(0, validators_1.IsNullable)(),
|
|
42
|
+
(0, class_validator_1.IsString)({ message: 'Search query must be a string' }),
|
|
43
|
+
(0, class_validator_1.MinLength)(1, { message: 'Search query cannot be empty' }),
|
|
44
|
+
(0, class_validator_1.MaxLength)(255, { message: 'Search query cannot exceed 255 characters' }),
|
|
45
|
+
__metadata("design:type", Object)
|
|
46
|
+
], GetLeadsDto.prototype, "search", void 0);
|
|
47
|
+
__decorate([
|
|
48
|
+
(0, class_validator_1.IsOptional)(),
|
|
49
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 1, { toClassOnly: true }),
|
|
50
|
+
(0, class_validator_1.IsNumber)({}, { message: 'Page must be a number' }),
|
|
51
|
+
(0, class_validator_1.Min)(1, { message: 'Page must be at least 1' }),
|
|
52
|
+
__metadata("design:type", Number)
|
|
53
|
+
], GetLeadsDto.prototype, "page", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
(0, class_validator_1.IsOptional)(),
|
|
56
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 10, { toClassOnly: true }),
|
|
57
|
+
(0, class_validator_1.IsNumber)({}, { message: 'Page size must be a number' }),
|
|
58
|
+
(0, class_validator_1.Min)(1, { message: 'Page size must be at least 1' }),
|
|
59
|
+
(0, class_validator_1.Max)(100, { message: 'Page size cannot exceed 100' }),
|
|
60
|
+
__metadata("design:type", Number)
|
|
61
|
+
], GetLeadsDto.prototype, "page_size", void 0);
|
|
62
|
+
__decorate([
|
|
63
|
+
(0, class_validator_1.IsOptional)(),
|
|
64
|
+
(0, validators_1.IsNullable)(),
|
|
65
|
+
(0, class_validator_1.IsDateString)({}, { message: 'Start date must be a valid ISO 8601 date string' }),
|
|
66
|
+
__metadata("design:type", Object)
|
|
67
|
+
], GetLeadsDto.prototype, "created_after", void 0);
|
|
68
|
+
__decorate([
|
|
69
|
+
(0, class_validator_1.IsOptional)(),
|
|
70
|
+
(0, validators_1.IsNullable)(),
|
|
71
|
+
(0, class_validator_1.IsDateString)({}, { message: 'End date must be a valid ISO 8601 date string' }),
|
|
72
|
+
__metadata("design:type", Object)
|
|
73
|
+
], GetLeadsDto.prototype, "created_before", void 0);
|
|
74
|
+
__decorate([
|
|
75
|
+
(0, class_validator_1.IsOptional)(),
|
|
76
|
+
(0, validators_1.IsNullable)(),
|
|
77
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? value.split(',').map((id) => id.trim()) : null, { toClassOnly: true }),
|
|
78
|
+
(0, class_validator_1.IsUUID)('7', { each: true, message: 'Each status ID must be a valid UUID v7' }),
|
|
79
|
+
__metadata("design:type", Object)
|
|
80
|
+
], GetLeadsDto.prototype, "status_id_in", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
(0, class_validator_1.IsOptional)(),
|
|
83
|
+
(0, validators_1.IsNullable)(),
|
|
84
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? value.split(',').map((id) => id.trim()) : null, { toClassOnly: true }),
|
|
85
|
+
(0, class_validator_1.IsUUID)('7', { each: true, message: 'Each status ID must be a valid UUID v7' }),
|
|
86
|
+
__metadata("design:type", Object)
|
|
87
|
+
], GetLeadsDto.prototype, "status_id_not_in", void 0);
|
|
88
|
+
__decorate([
|
|
89
|
+
(0, class_validator_1.IsOptional)(),
|
|
90
|
+
(0, validators_1.IsNullable)()
|
|
91
|
+
// v4: the owner is a `users.mms_id`, the GoTrue auth uid (see GetTasksDto).
|
|
92
|
+
,
|
|
93
|
+
(0, class_validator_1.IsUUID)('4', { message: 'Invalid assigned to ID format' }),
|
|
94
|
+
__metadata("design:type", Object)
|
|
95
|
+
], GetLeadsDto.prototype, "assigned_to_id", void 0);
|
|
96
|
+
__decorate([
|
|
97
|
+
(0, class_validator_1.IsOptional)(),
|
|
98
|
+
(0, validators_1.IsNullable)(),
|
|
99
|
+
(0, class_transformer_1.Transform)(({ value }) => value === 'true' ? true : value === 'false' ? false : value, { toClassOnly: true }),
|
|
100
|
+
(0, class_validator_1.IsBoolean)({ message: 'Completed must be true or false' }),
|
|
101
|
+
__metadata("design:type", Object)
|
|
102
|
+
], GetLeadsDto.prototype, "completed", void 0);
|
|
103
|
+
__decorate([
|
|
104
|
+
(0, class_validator_1.IsOptional)(),
|
|
105
|
+
(0, validators_1.IsNullable)(),
|
|
106
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? value === 'true' : false, { toClassOnly: true }),
|
|
107
|
+
(0, class_validator_1.IsBoolean)({ message: 'Include archived must be a boolean' }),
|
|
108
|
+
__metadata("design:type", Boolean)
|
|
109
|
+
], GetLeadsDto.prototype, "archive", void 0);
|
|
110
|
+
class LeadIdDto {
|
|
111
|
+
leadId;
|
|
112
|
+
}
|
|
113
|
+
exports.LeadIdDto = LeadIdDto;
|
|
114
|
+
__decorate([
|
|
115
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid lead ID format' }),
|
|
116
|
+
__metadata("design:type", String)
|
|
117
|
+
], LeadIdDto.prototype, "leadId", void 0);
|
|
118
|
+
class CreateLeadDto {
|
|
119
|
+
name;
|
|
120
|
+
contact_person;
|
|
121
|
+
email;
|
|
122
|
+
phone;
|
|
123
|
+
status_id;
|
|
124
|
+
// Omitted defaults to the creator. v4: a `users.mms_id` is the GoTrue auth uid.
|
|
125
|
+
assigned_to_id;
|
|
126
|
+
// The only check on the value: `leads.source` is plain text.
|
|
127
|
+
source;
|
|
128
|
+
estimated_value_minor;
|
|
129
|
+
follow_up_date;
|
|
130
|
+
// Required when the status is a fail status and cleared otherwise - `LeadsResource` decides.
|
|
131
|
+
lost_reason;
|
|
132
|
+
}
|
|
133
|
+
exports.CreateLeadDto = CreateLeadDto;
|
|
134
|
+
__decorate([
|
|
135
|
+
(0, class_validator_1.IsString)({ message: 'Invalid name' }),
|
|
136
|
+
(0, class_validator_1.MinLength)(1, { message: 'Name cannot be empty' }),
|
|
137
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Name cannot exceed 100 characters' }),
|
|
138
|
+
__metadata("design:type", String)
|
|
139
|
+
], CreateLeadDto.prototype, "name", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
(0, class_validator_1.IsOptional)(),
|
|
142
|
+
(0, validators_1.IsNullable)(),
|
|
143
|
+
(0, class_validator_1.IsString)({ message: 'Invalid contact person' }),
|
|
144
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Contact person cannot exceed 100 characters' }),
|
|
145
|
+
__metadata("design:type", Object)
|
|
146
|
+
], CreateLeadDto.prototype, "contact_person", void 0);
|
|
147
|
+
__decorate([
|
|
148
|
+
(0, class_validator_1.IsOptional)(),
|
|
149
|
+
(0, validators_1.IsNullable)(),
|
|
150
|
+
(0, class_validator_1.ValidateIf)((o) => o.email !== ''),
|
|
151
|
+
(0, class_validator_1.IsEmail)({}, { message: 'Invalid email address' }),
|
|
152
|
+
__metadata("design:type", Object)
|
|
153
|
+
], CreateLeadDto.prototype, "email", void 0);
|
|
154
|
+
__decorate([
|
|
155
|
+
(0, class_validator_1.IsOptional)(),
|
|
156
|
+
(0, validators_1.IsNullable)(),
|
|
157
|
+
(0, class_validator_1.IsString)({ message: 'Invalid phone number' }),
|
|
158
|
+
__metadata("design:type", Object)
|
|
159
|
+
], CreateLeadDto.prototype, "phone", void 0);
|
|
160
|
+
__decorate([
|
|
161
|
+
(0, class_validator_1.IsOptional)(),
|
|
162
|
+
(0, validators_1.IsNullable)(),
|
|
163
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid status ID format' }),
|
|
164
|
+
__metadata("design:type", Object)
|
|
165
|
+
], CreateLeadDto.prototype, "status_id", void 0);
|
|
166
|
+
__decorate([
|
|
167
|
+
(0, class_validator_1.IsOptional)(),
|
|
168
|
+
(0, validators_1.IsNullable)(),
|
|
169
|
+
(0, class_validator_1.IsUUID)('4', { message: 'Invalid assigned to ID format' }),
|
|
170
|
+
__metadata("design:type", Object)
|
|
171
|
+
], CreateLeadDto.prototype, "assigned_to_id", void 0);
|
|
172
|
+
__decorate([
|
|
173
|
+
(0, class_validator_1.IsOptional)(),
|
|
174
|
+
(0, validators_1.IsNullable)(),
|
|
175
|
+
(0, class_validator_1.IsEnum)(enum_1.LeadSource, { message: 'Invalid lead source' }),
|
|
176
|
+
__metadata("design:type", Object)
|
|
177
|
+
], CreateLeadDto.prototype, "source", void 0);
|
|
178
|
+
__decorate([
|
|
179
|
+
(0, class_validator_1.IsOptional)(),
|
|
180
|
+
(0, validators_1.IsNullable)(),
|
|
181
|
+
(0, class_validator_1.IsInt)({ message: 'Estimated value must be a whole number of minor units' }),
|
|
182
|
+
(0, class_validator_1.Min)(0, { message: 'Estimated value must be at least 0' }),
|
|
183
|
+
__metadata("design:type", Object)
|
|
184
|
+
], CreateLeadDto.prototype, "estimated_value_minor", void 0);
|
|
185
|
+
__decorate([
|
|
186
|
+
(0, class_validator_1.IsOptional)(),
|
|
187
|
+
(0, validators_1.IsNullable)(),
|
|
188
|
+
(0, validators_1.IsCalendarDate)({ message: 'Follow-up date must be a calendar date (YYYY-MM-DD)' }),
|
|
189
|
+
__metadata("design:type", Object)
|
|
190
|
+
], CreateLeadDto.prototype, "follow_up_date", void 0);
|
|
191
|
+
__decorate([
|
|
192
|
+
(0, class_validator_1.IsOptional)(),
|
|
193
|
+
(0, validators_1.IsNullable)(),
|
|
194
|
+
(0, class_validator_1.IsString)({ message: 'Invalid lost reason' }),
|
|
195
|
+
(0, class_validator_1.MaxLength)(500, { message: 'Lost reason cannot exceed 500 characters' }),
|
|
196
|
+
__metadata("design:type", Object)
|
|
197
|
+
], CreateLeadDto.prototype, "lost_reason", void 0);
|
|
198
|
+
class UpdateLeadDto extends CreateLeadDto {
|
|
199
|
+
}
|
|
200
|
+
exports.UpdateLeadDto = UpdateLeadDto;
|
|
201
|
+
class ConvertLeadDto {
|
|
202
|
+
client_status_id;
|
|
203
|
+
}
|
|
204
|
+
exports.ConvertLeadDto = ConvertLeadDto;
|
|
205
|
+
__decorate([
|
|
206
|
+
(0, validators_1.IsNullable)(),
|
|
207
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid status ID format' }),
|
|
208
|
+
__metadata("design:type", Object)
|
|
209
|
+
], ConvertLeadDto.prototype, "client_status_id", void 0);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ClientEntity } from '@managemint-solutions/entities/clients';
|
|
2
|
+
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
3
|
+
import type { LeadEntity, LeadSummary } from '@managemint-solutions/entities/leads';
|
|
4
|
+
import type { AuthContext } from '../auth';
|
|
5
|
+
import type { AdditionalDetailsResource } from '../additional-details';
|
|
6
|
+
import { type AuditResource } from '../audit';
|
|
7
|
+
import type { ClientsResource } from '../clients';
|
|
8
|
+
import type { TypedSupabaseClient } from '../client';
|
|
9
|
+
import type { ConvertLeadDto, CreateLeadDto, GetLeadsDto, UpdateLeadDto } from './dto';
|
|
10
|
+
export * from './dto';
|
|
11
|
+
export declare class LeadsResource {
|
|
12
|
+
private readonly supabase;
|
|
13
|
+
private readonly auth;
|
|
14
|
+
private readonly audit;
|
|
15
|
+
private readonly additionalDetails;
|
|
16
|
+
private readonly table;
|
|
17
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource, additionalDetails: AdditionalDetailsResource);
|
|
18
|
+
list(query: GetLeadsDto): Promise<MmsList<LeadSummary>>;
|
|
19
|
+
getById(leadId: string): Promise<LeadEntity>;
|
|
20
|
+
create(input: CreateLeadDto): Promise<LeadEntity>;
|
|
21
|
+
update(leadId: string, input: UpdateLeadDto): Promise<LeadEntity>;
|
|
22
|
+
archive(leadId: string): Promise<LeadEntity>;
|
|
23
|
+
unarchive(leadId: string): Promise<LeadEntity>;
|
|
24
|
+
restore(leadId: string): Promise<LeadEntity>;
|
|
25
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. A Won lead cannot be deleted. */
|
|
26
|
+
remove(leadId: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Turns the lead into a client: the client is created from the lead's name, email and phone
|
|
29
|
+
* with the status the caller picked, the lead's contact person, source and estimated value
|
|
30
|
+
* become additional details on it, the lead's notes, additional details and files move to it,
|
|
31
|
+
* and the lead moves to Won with `converted_client_id` and `converted_at` set. It is not
|
|
32
|
+
* deleted - it stays as a record of the deal, and from then on it cannot be changed
|
|
33
|
+
* (`refuseWonLead`). The list hides Won leads by default.
|
|
34
|
+
*
|
|
35
|
+
* There is no transaction across these writes (PostgREST and Storage cannot share one). A
|
|
36
|
+
* failure after the client is created leaves that client and a lead that is still not
|
|
37
|
+
* converted, holding whatever had not moved yet. Running the convert again is safe: step (a)
|
|
38
|
+
* only refuses a lead that finished converting, and steps (d) and (e) only touch rows still on
|
|
39
|
+
* the lead, so nothing is moved twice - but it makes a second client, and the first one (with
|
|
40
|
+
* whatever had already moved to it) is left for someone to tidy up or delete.
|
|
41
|
+
*
|
|
42
|
+
* Converting needs only `update_leads`: `clients` is the service-role clients resource
|
|
43
|
+
* acting for the caller (`forOrganization(org, user).clientsOnBehalfOfUser()`), so the
|
|
44
|
+
* client is created, and read back, without the caller holding any clients permission.
|
|
45
|
+
*/
|
|
46
|
+
convert(leadId: string, input: ConvertLeadDto, clients: ClientsResource): Promise<ClientEntity>;
|
|
47
|
+
/**
|
|
48
|
+
* The two status rules, checked before anything is written, and the lost reason the write
|
|
49
|
+
* stores. A lead reaches its Won status (`is_won`) only by converting, so choosing it is
|
|
50
|
+
* refused (a converted lead is not updated at all). A fail status needs a lost reason; any
|
|
51
|
+
* other status, or none, clears it.
|
|
52
|
+
*/
|
|
53
|
+
private lostReasonFor;
|
|
54
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.LeadsResource = void 0;
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/leads/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/additional-details/enum");
|
|
20
|
+
const enum_3 = require("@managemint-solutions/entities/notes/enum");
|
|
21
|
+
const enum_4 = require("@managemint-solutions/entities/status/enum");
|
|
22
|
+
const enum_5 = require("@managemint-solutions/entities/supporting-files/enum");
|
|
23
|
+
const enum_6 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
24
|
+
const audit_1 = require("../audit");
|
|
25
|
+
const errors_1 = require("../errors");
|
|
26
|
+
const paths_1 = require("../supporting-files/paths");
|
|
27
|
+
const transforms_1 = require("../transforms");
|
|
28
|
+
const won_1 = require("./won");
|
|
29
|
+
__exportStar(require("./dto"), exports);
|
|
30
|
+
// The public contract is the shared LeadSummary / LeadEntity from @managemint-solutions/entities;
|
|
31
|
+
// the casts at the returns declare the shape the select strings below produce (the columns are
|
|
32
|
+
// defined in the api's supabase/migrations).
|
|
33
|
+
const LEAD_LIST_SELECT = `
|
|
34
|
+
mms_id,
|
|
35
|
+
created_at,
|
|
36
|
+
name,
|
|
37
|
+
contact_person,
|
|
38
|
+
email,
|
|
39
|
+
status:statuses!status_id (
|
|
40
|
+
mms_id,
|
|
41
|
+
colour,
|
|
42
|
+
label,
|
|
43
|
+
is_complete,
|
|
44
|
+
is_fail,
|
|
45
|
+
is_triage,
|
|
46
|
+
is_won
|
|
47
|
+
),
|
|
48
|
+
assigned_to:user_profiles!assigned_to_id (*),
|
|
49
|
+
estimated_value_minor,
|
|
50
|
+
follow_up_date,
|
|
51
|
+
created_by:user_profiles!created_by (*)
|
|
52
|
+
`;
|
|
53
|
+
const LEAD_DETAIL_SELECT = `
|
|
54
|
+
mms_id,
|
|
55
|
+
created_at,
|
|
56
|
+
name,
|
|
57
|
+
contact_person,
|
|
58
|
+
email,
|
|
59
|
+
phone,
|
|
60
|
+
status:statuses!status_id (
|
|
61
|
+
mms_id,
|
|
62
|
+
label,
|
|
63
|
+
colour,
|
|
64
|
+
is_complete,
|
|
65
|
+
is_fail,
|
|
66
|
+
is_triage,
|
|
67
|
+
is_won
|
|
68
|
+
),
|
|
69
|
+
assigned_to:user_profiles!assigned_to_id ${transforms_1.USER_PROFILE_EMBED},
|
|
70
|
+
source,
|
|
71
|
+
estimated_value_minor,
|
|
72
|
+
follow_up_date,
|
|
73
|
+
lost_reason,
|
|
74
|
+
converted_client:client_profiles!converted_client_id (*),
|
|
75
|
+
converted_at,
|
|
76
|
+
updated_at,
|
|
77
|
+
last_updated_by:user_profiles!last_updated_by ${transforms_1.USER_PROFILE_EMBED},
|
|
78
|
+
created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
|
|
79
|
+
archived,
|
|
80
|
+
archived_at,
|
|
81
|
+
archived_by:user_profiles!archived_by ${transforms_1.USER_PROFILE_EMBED},
|
|
82
|
+
unarchived_at,
|
|
83
|
+
unarchived_by:user_profiles!unarchived_by ${transforms_1.USER_PROFILE_EMBED},
|
|
84
|
+
deleted,
|
|
85
|
+
deleted_at,
|
|
86
|
+
deleted_by:user_profiles!deleted_by ${transforms_1.USER_PROFILE_EMBED},
|
|
87
|
+
restored_at,
|
|
88
|
+
restored_by:user_profiles!restored_by ${transforms_1.USER_PROFILE_EMBED}
|
|
89
|
+
`;
|
|
90
|
+
// What `convert` reads of the lead: what the client and its additional details are made from,
|
|
91
|
+
// the two flags that refuse it, and the organization's currency for the estimated value.
|
|
92
|
+
const LEAD_CONVERT_SELECT = 'name, email, phone, contact_person, source, estimated_value_minor, deleted, converted_client_id, organization:organizations!organization_id ( currency )';
|
|
93
|
+
/** How a source reads in the "Lead source" additional detail the convert writes. */
|
|
94
|
+
const LEAD_SOURCE_LABELS = {
|
|
95
|
+
[enum_1.LeadSource.REFERRAL]: 'Referral',
|
|
96
|
+
[enum_1.LeadSource.WEBSITE]: 'Website',
|
|
97
|
+
[enum_1.LeadSource.SOCIAL_MEDIA]: 'Social media',
|
|
98
|
+
[enum_1.LeadSource.EVENT]: 'Event',
|
|
99
|
+
[enum_1.LeadSource.COLD_OUTREACH]: 'Cold outreach',
|
|
100
|
+
[enum_1.LeadSource.EXISTING_CLIENT]: 'Existing client',
|
|
101
|
+
[enum_1.LeadSource.OTHER]: 'Other',
|
|
102
|
+
};
|
|
103
|
+
/** Storage bucket every supporting file lives in (as in SupportingFilesResource). */
|
|
104
|
+
const SUPPORTING_FILES_BUCKET = 'supporting-files';
|
|
105
|
+
const refuse = (status, message) => new errors_1.SupabaseClientError({ status, error: 'Database Error', message });
|
|
106
|
+
class LeadsResource {
|
|
107
|
+
supabase;
|
|
108
|
+
auth;
|
|
109
|
+
audit;
|
|
110
|
+
additionalDetails;
|
|
111
|
+
table;
|
|
112
|
+
constructor(supabase, auth, audit, additionalDetails) {
|
|
113
|
+
this.supabase = supabase;
|
|
114
|
+
this.auth = auth;
|
|
115
|
+
this.audit = audit;
|
|
116
|
+
this.additionalDetails = additionalDetails;
|
|
117
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
118
|
+
table: 'leads',
|
|
119
|
+
entityType: enum_6.AuditTrailEntityType.LEADS,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async list(query) {
|
|
123
|
+
const page = query.page;
|
|
124
|
+
const pageSize = query.page_size;
|
|
125
|
+
const offset = (page - 1) * pageSize;
|
|
126
|
+
const trimmedSearch = query.search?.trim();
|
|
127
|
+
// `order`/`range` return `this`, so the builder stays a filter builder and the conditional
|
|
128
|
+
// filters below can be reassigned onto it without a cast.
|
|
129
|
+
let leadsQuery = this.supabase
|
|
130
|
+
.from('leads')
|
|
131
|
+
.select(LEAD_LIST_SELECT, { count: 'estimated' })
|
|
132
|
+
.eq('organization_id', this.auth.organization_id)
|
|
133
|
+
.eq('archived', query.archive)
|
|
134
|
+
.eq('deleted', false)
|
|
135
|
+
.order('created_at', { ascending: false })
|
|
136
|
+
.range(offset, offset + pageSize - 1);
|
|
137
|
+
if (trimmedSearch) {
|
|
138
|
+
leadsQuery = leadsQuery.or((0, transforms_1.orIlike)(['name', 'contact_person', 'email'], trimmedSearch));
|
|
139
|
+
}
|
|
140
|
+
if (query.assigned_to_id) {
|
|
141
|
+
leadsQuery = leadsQuery.eq('assigned_to_id', query.assigned_to_id);
|
|
142
|
+
}
|
|
143
|
+
if (query.created_after) {
|
|
144
|
+
leadsQuery = leadsQuery.gte('created_at', query.created_after);
|
|
145
|
+
}
|
|
146
|
+
if (query.created_before) {
|
|
147
|
+
leadsQuery = leadsQuery.lte('created_at', query.created_before);
|
|
148
|
+
}
|
|
149
|
+
if (query.status_id_in && query.status_id_in.length > 0) {
|
|
150
|
+
leadsQuery = leadsQuery.in('status_id', query.status_id_in);
|
|
151
|
+
}
|
|
152
|
+
// "No status OR not one of these", as tasks: a lead with no status is not won, so it stays in
|
|
153
|
+
// the open view. The ids are `@IsUUID('7')`-validated by the DTO, so interpolating them is
|
|
154
|
+
// safe.
|
|
155
|
+
if (query.status_id_not_in && query.status_id_not_in.length > 0) {
|
|
156
|
+
leadsQuery = leadsQuery.or(`status_id.is.null,status_id.not.in.(${query.status_id_not_in.join(',')})`);
|
|
157
|
+
}
|
|
158
|
+
const { data, error, count } = await leadsQuery;
|
|
159
|
+
if (error)
|
|
160
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
161
|
+
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
162
|
+
}
|
|
163
|
+
async getById(leadId) {
|
|
164
|
+
const { data, error } = await this.supabase
|
|
165
|
+
.from('leads')
|
|
166
|
+
.select(LEAD_DETAIL_SELECT)
|
|
167
|
+
.eq('organization_id', this.auth.organization_id)
|
|
168
|
+
.eq('mms_id', leadId)
|
|
169
|
+
.single();
|
|
170
|
+
if (error)
|
|
171
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
172
|
+
// A soft-deleted lead keeps its audit trail but gives up its details. The client it became
|
|
173
|
+
// stays, so a converted lead can still point at it.
|
|
174
|
+
if (data.deleted) {
|
|
175
|
+
return {
|
|
176
|
+
...data,
|
|
177
|
+
name: '',
|
|
178
|
+
contact_person: null,
|
|
179
|
+
email: '',
|
|
180
|
+
phone: '',
|
|
181
|
+
status: null,
|
|
182
|
+
assigned_to: null,
|
|
183
|
+
source: null,
|
|
184
|
+
estimated_value_minor: null,
|
|
185
|
+
follow_up_date: null,
|
|
186
|
+
lost_reason: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
191
|
+
async create(input) {
|
|
192
|
+
const lostReason = await this.lostReasonFor(input.status_id ?? null, input.lost_reason);
|
|
193
|
+
const row = await this.table.insert({
|
|
194
|
+
...input,
|
|
195
|
+
// Omitted means the creator owns the lead; an explicit null leaves it unassigned.
|
|
196
|
+
assigned_to_id: input.assigned_to_id === undefined ? this.auth.mms_id : input.assigned_to_id,
|
|
197
|
+
lost_reason: lostReason,
|
|
198
|
+
organization_id: this.auth.organization_id,
|
|
199
|
+
created_by: this.auth.mms_id,
|
|
200
|
+
created_at: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
return this.getById(row.mms_id);
|
|
203
|
+
}
|
|
204
|
+
async update(leadId, input) {
|
|
205
|
+
// A Won lead is final. An update that leaves the status out keeps the lead's own, and the
|
|
206
|
+
// rules are checked against that one.
|
|
207
|
+
const { data, error } = await this.supabase
|
|
208
|
+
.from('leads')
|
|
209
|
+
.select('status_id, converted_client_id')
|
|
210
|
+
.eq('organization_id', this.auth.organization_id)
|
|
211
|
+
.eq('mms_id', leadId)
|
|
212
|
+
.single();
|
|
213
|
+
if (error)
|
|
214
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
215
|
+
const current = data;
|
|
216
|
+
if (current.converted_client_id) {
|
|
217
|
+
throw refuse(400, 'This lead is Won and can no longer be changed');
|
|
218
|
+
}
|
|
219
|
+
const statusId = input.status_id === undefined ? current.status_id : input.status_id;
|
|
220
|
+
const lostReason = await this.lostReasonFor(statusId, input.lost_reason);
|
|
221
|
+
await this.table.update(leadId, {
|
|
222
|
+
...input,
|
|
223
|
+
lost_reason: lostReason,
|
|
224
|
+
last_updated_by: this.auth.mms_id,
|
|
225
|
+
updated_at: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
return this.getById(leadId);
|
|
228
|
+
}
|
|
229
|
+
async archive(leadId) {
|
|
230
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, leadId);
|
|
231
|
+
await this.table.transition(leadId, enum_6.AuditTrailAction.ARCHIVE, (0, transforms_1.archivedPatch)(this.auth.mms_id));
|
|
232
|
+
return this.getById(leadId);
|
|
233
|
+
}
|
|
234
|
+
async unarchive(leadId) {
|
|
235
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, leadId);
|
|
236
|
+
await this.table.transition(leadId, enum_6.AuditTrailAction.UNARCHIVE, (0, transforms_1.unarchivedPatch)(this.auth.mms_id));
|
|
237
|
+
return this.getById(leadId);
|
|
238
|
+
}
|
|
239
|
+
async restore(leadId) {
|
|
240
|
+
await this.table.transition(leadId, enum_6.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
|
|
241
|
+
return this.getById(leadId);
|
|
242
|
+
}
|
|
243
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. A Won lead cannot be deleted. */
|
|
244
|
+
async remove(leadId) {
|
|
245
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, leadId);
|
|
246
|
+
await this.table.transition(leadId, enum_6.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Turns the lead into a client: the client is created from the lead's name, email and phone
|
|
250
|
+
* with the status the caller picked, the lead's contact person, source and estimated value
|
|
251
|
+
* become additional details on it, the lead's notes, additional details and files move to it,
|
|
252
|
+
* and the lead moves to Won with `converted_client_id` and `converted_at` set. It is not
|
|
253
|
+
* deleted - it stays as a record of the deal, and from then on it cannot be changed
|
|
254
|
+
* (`refuseWonLead`). The list hides Won leads by default.
|
|
255
|
+
*
|
|
256
|
+
* There is no transaction across these writes (PostgREST and Storage cannot share one). A
|
|
257
|
+
* failure after the client is created leaves that client and a lead that is still not
|
|
258
|
+
* converted, holding whatever had not moved yet. Running the convert again is safe: step (a)
|
|
259
|
+
* only refuses a lead that finished converting, and steps (d) and (e) only touch rows still on
|
|
260
|
+
* the lead, so nothing is moved twice - but it makes a second client, and the first one (with
|
|
261
|
+
* whatever had already moved to it) is left for someone to tidy up or delete.
|
|
262
|
+
*
|
|
263
|
+
* Converting needs only `update_leads`: `clients` is the service-role clients resource
|
|
264
|
+
* acting for the caller (`forOrganization(org, user).clientsOnBehalfOfUser()`), so the
|
|
265
|
+
* client is created, and read back, without the caller holding any clients permission.
|
|
266
|
+
*/
|
|
267
|
+
async convert(leadId, input, clients) {
|
|
268
|
+
// (a) The lead, in this organization, not deleted and not converted already.
|
|
269
|
+
const { data, error } = await this.supabase
|
|
270
|
+
.from('leads')
|
|
271
|
+
.select(LEAD_CONVERT_SELECT)
|
|
272
|
+
.eq('organization_id', this.auth.organization_id)
|
|
273
|
+
.eq('mms_id', leadId)
|
|
274
|
+
.maybeSingle();
|
|
275
|
+
if (error)
|
|
276
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
277
|
+
const lead = data;
|
|
278
|
+
if (lead?.converted_client_id) {
|
|
279
|
+
throw refuse(400, 'This lead has already been converted');
|
|
280
|
+
}
|
|
281
|
+
if (!lead || lead.deleted) {
|
|
282
|
+
throw refuse(404, 'Lead not found');
|
|
283
|
+
}
|
|
284
|
+
// (b) The client, service-role for the caller, audited as their INSERT.
|
|
285
|
+
const client = await clients.create({
|
|
286
|
+
name: lead.name,
|
|
287
|
+
email: lead.email,
|
|
288
|
+
phone: lead.phone,
|
|
289
|
+
status_id: input.client_status_id,
|
|
290
|
+
contract_client: false,
|
|
291
|
+
contracted_hours: null,
|
|
292
|
+
});
|
|
293
|
+
const clientId = client.mms_id;
|
|
294
|
+
// (c) The lead fields a client has no column for, as additional details - only those set.
|
|
295
|
+
const details = [];
|
|
296
|
+
if (lead.contact_person) {
|
|
297
|
+
details.push({ key: 'Contact person', value: lead.contact_person });
|
|
298
|
+
}
|
|
299
|
+
if (lead.source) {
|
|
300
|
+
details.push({ key: 'Lead source', value: LEAD_SOURCE_LABELS[lead.source] ?? lead.source });
|
|
301
|
+
}
|
|
302
|
+
if (lead.estimated_value_minor !== null) {
|
|
303
|
+
const amount = (lead.estimated_value_minor / 100).toFixed(2);
|
|
304
|
+
details.push({
|
|
305
|
+
key: 'Estimated value',
|
|
306
|
+
value: lead.organization?.currency ? `${amount} ${lead.organization.currency}` : amount,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
for (const detail of details) {
|
|
310
|
+
await this.additionalDetails.create(enum_2.AdditionalDetailsEntityTypes.CLIENT, clientId, {
|
|
311
|
+
key: detail.key,
|
|
312
|
+
value: detail.value,
|
|
313
|
+
type: enum_2.AdditionalDetailsTypes.Text,
|
|
314
|
+
copyable: false,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// (d) The lead's notes and additional details now hang off the client.
|
|
318
|
+
const moves = [
|
|
319
|
+
['notes', enum_3.NotesEntityTypes.LEAD, enum_3.NotesEntityTypes.CLIENT],
|
|
320
|
+
['additional_details', enum_2.AdditionalDetailsEntityTypes.LEAD, enum_2.AdditionalDetailsEntityTypes.CLIENT],
|
|
321
|
+
];
|
|
322
|
+
for (const [table, fromType, toType] of moves) {
|
|
323
|
+
const { error: moveError } = await this.supabase
|
|
324
|
+
.from(table)
|
|
325
|
+
.update({ entity_type: toType, entity_id: clientId })
|
|
326
|
+
.eq('organization_id', this.auth.organization_id)
|
|
327
|
+
.eq('entity_type', fromType)
|
|
328
|
+
.eq('entity_id', leadId);
|
|
329
|
+
if (moveError)
|
|
330
|
+
throw (0, errors_1.mapPostgrestError)(moveError);
|
|
331
|
+
}
|
|
332
|
+
// (e) Each supporting file: the object moves to the client's folder, then its row follows.
|
|
333
|
+
// An object already missing from storage is skipped - there is nothing to move - and its row
|
|
334
|
+
// stays on the lead.
|
|
335
|
+
const { data: files, error: filesError } = await this.supabase
|
|
336
|
+
.from('supporting_files')
|
|
337
|
+
.select('mms_id, name, extension')
|
|
338
|
+
.eq('organization_id', this.auth.organization_id)
|
|
339
|
+
.eq('entity_type', enum_5.SupportingFilesEntityTypes.LEAD)
|
|
340
|
+
.eq('entity_id', leadId);
|
|
341
|
+
if (filesError)
|
|
342
|
+
throw (0, errors_1.mapPostgrestError)(filesError);
|
|
343
|
+
for (const file of files) {
|
|
344
|
+
const { error: storageError } = await this.supabase.storage
|
|
345
|
+
.from(SUPPORTING_FILES_BUCKET)
|
|
346
|
+
.move((0, paths_1.storagePath)(this.auth.organization_id, enum_5.SupportingFilesEntityTypes.LEAD, leadId, file.name, file.extension), (0, paths_1.storagePath)(this.auth.organization_id, enum_5.SupportingFilesEntityTypes.CLIENT, clientId, file.name, file.extension));
|
|
347
|
+
if (storageError) {
|
|
348
|
+
if (storageError.status === 404 || storageError.statusCode === '404')
|
|
349
|
+
continue;
|
|
350
|
+
throw (0, errors_1.mapStorageError)(storageError);
|
|
351
|
+
}
|
|
352
|
+
const { error: rowError } = await this.supabase
|
|
353
|
+
.from('supporting_files')
|
|
354
|
+
.update({ entity_type: enum_5.SupportingFilesEntityTypes.CLIENT, entity_id: clientId })
|
|
355
|
+
.eq('organization_id', this.auth.organization_id)
|
|
356
|
+
.eq('mms_id', file.mms_id);
|
|
357
|
+
if (rowError)
|
|
358
|
+
throw (0, errors_1.mapPostgrestError)(rowError);
|
|
359
|
+
}
|
|
360
|
+
// (f) The lead moves to its organization's Won status (`is_won`, one per organization, when
|
|
361
|
+
// it has one) and is linked to the client it became, audited as an UPDATE. It is not deleted.
|
|
362
|
+
const { data: won, error: wonError } = await this.supabase
|
|
363
|
+
.from('statuses')
|
|
364
|
+
.select('mms_id')
|
|
365
|
+
.eq('organization_id', this.auth.organization_id)
|
|
366
|
+
.eq('is_won', true)
|
|
367
|
+
.maybeSingle();
|
|
368
|
+
if (wonError)
|
|
369
|
+
throw (0, errors_1.mapPostgrestError)(wonError);
|
|
370
|
+
await this.table.update(leadId, {
|
|
371
|
+
...(won && { status_id: won.mms_id, lost_reason: null }),
|
|
372
|
+
converted_client_id: clientId,
|
|
373
|
+
converted_at: new Date().toISOString(),
|
|
374
|
+
last_updated_by: this.auth.mms_id,
|
|
375
|
+
updated_at: new Date().toISOString(),
|
|
376
|
+
});
|
|
377
|
+
// (g) The client as the clients resource reads it.
|
|
378
|
+
return clients.getById(clientId);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The two status rules, checked before anything is written, and the lost reason the write
|
|
382
|
+
* stores. A lead reaches its Won status (`is_won`) only by converting, so choosing it is
|
|
383
|
+
* refused (a converted lead is not updated at all). A fail status needs a lost reason; any
|
|
384
|
+
* other status, or none, clears it.
|
|
385
|
+
*/
|
|
386
|
+
async lostReasonFor(statusId, lostReason) {
|
|
387
|
+
if (!statusId)
|
|
388
|
+
return null;
|
|
389
|
+
const { data, error } = await this.supabase
|
|
390
|
+
.from('statuses')
|
|
391
|
+
.select('is_fail, is_won')
|
|
392
|
+
.eq('organization_id', this.auth.organization_id)
|
|
393
|
+
.eq('entity_type', enum_4.StatusEntityTypes.LEAD)
|
|
394
|
+
.eq('mms_id', statusId)
|
|
395
|
+
.maybeSingle();
|
|
396
|
+
if (error)
|
|
397
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
398
|
+
// A status that is not one of this organization's lead statuses is not a fail status; the
|
|
399
|
+
// leads status trigger refuses it on the write.
|
|
400
|
+
const status = data;
|
|
401
|
+
if (status?.is_won) {
|
|
402
|
+
throw refuse(400, 'Convert the lead to a client to mark it Won');
|
|
403
|
+
}
|
|
404
|
+
if (!status?.is_fail)
|
|
405
|
+
return null;
|
|
406
|
+
const reason = lostReason?.trim();
|
|
407
|
+
if (!reason) {
|
|
408
|
+
throw refuse(400, 'Give a reason when marking a lead as lost');
|
|
409
|
+
}
|
|
410
|
+
return reason;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
exports.LeadsResource = LeadsResource;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TypedSupabaseClient } from '../client';
|
|
2
|
+
/**
|
|
3
|
+
* A converted lead is Won and final: its fields, its lifecycle and what hangs off it (notes,
|
|
4
|
+
* additional details, supporting files) no longer change. Throws 400 when the lead is converted;
|
|
5
|
+
* a missing lead is left to the write that follows.
|
|
6
|
+
*/
|
|
7
|
+
export declare function refuseWonLead(supabase: TypedSupabaseClient, organizationId: string, leadId: string): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.refuseWonLead = refuseWonLead;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
/**
|
|
6
|
+
* A converted lead is Won and final: its fields, its lifecycle and what hangs off it (notes,
|
|
7
|
+
* additional details, supporting files) no longer change. Throws 400 when the lead is converted;
|
|
8
|
+
* a missing lead is left to the write that follows.
|
|
9
|
+
*/
|
|
10
|
+
async function refuseWonLead(supabase, organizationId, leadId) {
|
|
11
|
+
const { data, error } = await supabase
|
|
12
|
+
.from('leads')
|
|
13
|
+
.select('converted_client_id')
|
|
14
|
+
.eq('organization_id', organizationId)
|
|
15
|
+
.eq('mms_id', leadId)
|
|
16
|
+
.maybeSingle();
|
|
17
|
+
if (error)
|
|
18
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
19
|
+
if (data?.converted_client_id) {
|
|
20
|
+
throw new errors_1.SupabaseClientError({
|
|
21
|
+
status: 400,
|
|
22
|
+
error: 'Database Error',
|
|
23
|
+
message: 'This lead is Won and can no longer be changed',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/notes/index.js
CHANGED
|
@@ -15,9 +15,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.NotesResource = void 0;
|
|
18
|
-
const enum_1 = require("@managemint-solutions/entities/
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/notes/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
20
|
const audit_1 = require("../audit");
|
|
20
21
|
const errors_1 = require("../errors");
|
|
22
|
+
const won_1 = require("../leads/won");
|
|
21
23
|
__exportStar(require("./dto"), exports);
|
|
22
24
|
// The public contract is the shared NoteEntity from @managemint-solutions/entities; the casts at
|
|
23
25
|
// the returns declare the shape the select string below produces (the columns are defined in the
|
|
@@ -32,7 +34,7 @@ class NotesResource {
|
|
|
32
34
|
this.auth = auth;
|
|
33
35
|
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
34
36
|
table: 'notes',
|
|
35
|
-
entityType:
|
|
37
|
+
entityType: enum_2.AuditTrailEntityType.NOTES,
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
async getById(noteId) {
|
|
@@ -59,6 +61,9 @@ class NotesResource {
|
|
|
59
61
|
return data;
|
|
60
62
|
}
|
|
61
63
|
async create(entity, entityId, input) {
|
|
64
|
+
if (entity === enum_1.NotesEntityTypes.LEAD) {
|
|
65
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
|
|
66
|
+
}
|
|
62
67
|
const row = await this.table.insert({
|
|
63
68
|
content: input.content,
|
|
64
69
|
entity_type: entity,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { UpdatePermissionsDto as UpdatePermissionsDtoType } from '@managemint-solutions/entities/users/permissions/dto';
|
|
2
2
|
export declare class UpdatePermissionsDto implements UpdatePermissionsDtoType {
|
|
3
|
+
readonly create_leads: boolean;
|
|
4
|
+
readonly read_leads: boolean;
|
|
5
|
+
readonly update_leads: boolean;
|
|
6
|
+
readonly delete_leads: boolean;
|
|
3
7
|
readonly create_clients: boolean;
|
|
4
8
|
readonly read_clients: boolean;
|
|
5
9
|
readonly update_clients: boolean;
|
package/dist/permissions/dto.js
CHANGED
|
@@ -14,6 +14,10 @@ const class_validator_1 = require("class-validator");
|
|
|
14
14
|
class UpdatePermissionsDto {
|
|
15
15
|
// @IsBoolean({ message: 'View Dashboard must be a boolean' })
|
|
16
16
|
// readonly view_dashboard!: boolean;
|
|
17
|
+
create_leads;
|
|
18
|
+
read_leads;
|
|
19
|
+
update_leads;
|
|
20
|
+
delete_leads;
|
|
17
21
|
create_clients;
|
|
18
22
|
read_clients;
|
|
19
23
|
update_clients;
|
|
@@ -108,6 +112,22 @@ class UpdatePermissionsDto {
|
|
|
108
112
|
read_logs;
|
|
109
113
|
}
|
|
110
114
|
exports.UpdatePermissionsDto = UpdatePermissionsDto;
|
|
115
|
+
__decorate([
|
|
116
|
+
(0, class_validator_1.IsBoolean)({ message: 'Create Leads must be a boolean' }),
|
|
117
|
+
__metadata("design:type", Boolean)
|
|
118
|
+
], UpdatePermissionsDto.prototype, "create_leads", void 0);
|
|
119
|
+
__decorate([
|
|
120
|
+
(0, class_validator_1.IsBoolean)({ message: 'Read Leads must be a boolean' }),
|
|
121
|
+
__metadata("design:type", Boolean)
|
|
122
|
+
], UpdatePermissionsDto.prototype, "read_leads", void 0);
|
|
123
|
+
__decorate([
|
|
124
|
+
(0, class_validator_1.IsBoolean)({ message: 'Update Leads must be a boolean' }),
|
|
125
|
+
__metadata("design:type", Boolean)
|
|
126
|
+
], UpdatePermissionsDto.prototype, "update_leads", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
(0, class_validator_1.IsBoolean)({ message: 'Delete Leads must be a boolean' }),
|
|
129
|
+
__metadata("design:type", Boolean)
|
|
130
|
+
], UpdatePermissionsDto.prototype, "delete_leads", void 0);
|
|
111
131
|
__decorate([
|
|
112
132
|
(0, class_validator_1.IsBoolean)({ message: 'Create Clients must be a boolean' }),
|
|
113
133
|
__metadata("design:type", Boolean)
|
package/dist/pricebooks/index.js
CHANGED
|
@@ -15,12 +15,13 @@ const PRICEBOOK_SELECT = `
|
|
|
15
15
|
unit_amount_minor,
|
|
16
16
|
currency,
|
|
17
17
|
billing_modules (
|
|
18
|
-
module_key
|
|
18
|
+
module_key,
|
|
19
|
+
name
|
|
19
20
|
)
|
|
20
21
|
)
|
|
21
22
|
`;
|
|
22
|
-
// A pricebook_modules row whose catalog entry did not come back leaves `module_key`
|
|
23
|
-
// the cast keeps the mapping honest about that rather than inventing
|
|
23
|
+
// A pricebook_modules row whose catalog entry did not come back leaves `module_key` and `name`
|
|
24
|
+
// undefined - the cast keeps the mapping honest about that rather than inventing them.
|
|
24
25
|
const toPricebookWithModules = (row) => ({
|
|
25
26
|
mms_id: row.mms_id,
|
|
26
27
|
name: row.name,
|
|
@@ -29,6 +30,7 @@ const toPricebookWithModules = (row) => ({
|
|
|
29
30
|
modules: (row.pricebook_modules || []).map((pricedModule) => ({
|
|
30
31
|
module_id: pricedModule.module_id,
|
|
31
32
|
module_key: pricedModule.billing_modules?.module_key,
|
|
33
|
+
name: pricedModule.billing_modules?.name,
|
|
32
34
|
unit_amount_minor: pricedModule.unit_amount_minor,
|
|
33
35
|
currency: pricedModule.currency,
|
|
34
36
|
})),
|
package/dist/projects/index.js
CHANGED
|
@@ -35,7 +35,8 @@ const PROJECT_LIST_SELECT = `
|
|
|
35
35
|
label,
|
|
36
36
|
is_complete,
|
|
37
37
|
is_fail,
|
|
38
|
-
is_triage
|
|
38
|
+
is_triage,
|
|
39
|
+
is_won
|
|
39
40
|
),
|
|
40
41
|
created_by:user_profiles!created_by (*),
|
|
41
42
|
manager:user_profiles!manager_id (*),
|
|
@@ -52,7 +53,8 @@ const PROJECT_DETAIL_SELECT = `
|
|
|
52
53
|
colour,
|
|
53
54
|
is_complete,
|
|
54
55
|
is_fail,
|
|
55
|
-
is_triage
|
|
56
|
+
is_triage,
|
|
57
|
+
is_won
|
|
56
58
|
),
|
|
57
59
|
has_budget,
|
|
58
60
|
start_date,
|
|
@@ -2,6 +2,7 @@ import { AuditResource, type AuditActor, type AuditedTable, type AuditedTableOpt
|
|
|
2
2
|
import type { TypedSupabaseClient } from '../client';
|
|
3
3
|
import { BillingResource } from '../billing';
|
|
4
4
|
import { InvoicesResource } from '../billing/invoices';
|
|
5
|
+
import { ClientsResource } from '../clients';
|
|
5
6
|
import { ConfigsResource } from '../configs';
|
|
6
7
|
import { FeatureRequestsResource } from '../feature-requests';
|
|
7
8
|
import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from '../leave';
|
|
@@ -45,6 +46,12 @@ export declare class SupabaseOrganizationScope {
|
|
|
45
46
|
* subscriptions and both webhook handler services.
|
|
46
47
|
*/
|
|
47
48
|
table(options: AuditedTableOptions): AuditedTable;
|
|
49
|
+
/**
|
|
50
|
+
* Clients written service-role on a user's behalf: converting a lead needs only
|
|
51
|
+
* `update_leads`, so the client it becomes is created here rather than through the caller's
|
|
52
|
+
* own client, and is still attributed to that user. The scope must carry the user.
|
|
53
|
+
*/
|
|
54
|
+
clientsOnBehalfOfUser(): ClientsResource;
|
|
48
55
|
}
|
|
49
56
|
/**
|
|
50
57
|
* The client for work that has no caller and no RLS context to run under: inbound webhooks
|
|
@@ -6,6 +6,7 @@ const audit_1 = require("../audit");
|
|
|
6
6
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
7
7
|
const billing_1 = require("../billing");
|
|
8
8
|
const invoices_1 = require("../billing/invoices");
|
|
9
|
+
const clients_1 = require("../clients");
|
|
9
10
|
const configs_1 = require("../configs");
|
|
10
11
|
const feature_requests_1 = require("../feature-requests");
|
|
11
12
|
const leave_1 = require("../leave");
|
|
@@ -60,6 +61,17 @@ class SupabaseOrganizationScope {
|
|
|
60
61
|
table(options) {
|
|
61
62
|
return (0, audit_1.auditedTable)(this.supabase, this.actor, this.audit, options);
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Clients written service-role on a user's behalf: converting a lead needs only
|
|
66
|
+
* `update_leads`, so the client it becomes is created here rather than through the caller's
|
|
67
|
+
* own client, and is still attributed to that user. The scope must carry the user.
|
|
68
|
+
*/
|
|
69
|
+
clientsOnBehalfOfUser() {
|
|
70
|
+
const { mms_id, organization_id } = this.actor;
|
|
71
|
+
if (!mms_id)
|
|
72
|
+
throw new Error('clientsOnBehalfOfUser needs a user actor');
|
|
73
|
+
return new clients_1.ClientsResource(this.supabase, { mms_id, organization_id }, this.audit);
|
|
74
|
+
}
|
|
63
75
|
}
|
|
64
76
|
exports.SupabaseOrganizationScope = SupabaseOrganizationScope;
|
|
65
77
|
/**
|
package/dist/statuses/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StatusEntity } from '@managemint-solutions/entities/status';
|
|
2
2
|
import type { AddStatusDto, UpdateStatusDto } from '@managemint-solutions/entities/status/dto';
|
|
3
|
-
import { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
|
|
3
|
+
import type { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
|
|
4
4
|
import type { AuthContext } from '../auth';
|
|
5
5
|
import { type AuditResource } from '../audit';
|
|
6
6
|
import type { TypedSupabaseClient } from '../client';
|
|
@@ -13,5 +13,12 @@ export declare class StatusesResource {
|
|
|
13
13
|
list(entity: StatusEntityTypes): Promise<StatusEntity[]>;
|
|
14
14
|
create(input: AddStatusDto): Promise<StatusEntity>;
|
|
15
15
|
update(statusId: string, input: UpdateStatusDto): Promise<StatusEntity>;
|
|
16
|
+
/** The Won status cannot be deleted. Every other status can. */
|
|
16
17
|
remove(statusId: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the status is this organization's Won lead status (`is_won`), where converting a
|
|
20
|
+
* lead moves it. Won is fixed: it cannot be edited or deleted. Every other status - a lead
|
|
21
|
+
* status the organization marks complete included - is ordinary.
|
|
22
|
+
*/
|
|
23
|
+
private isWon;
|
|
17
24
|
}
|
package/dist/statuses/index.js
CHANGED
|
@@ -19,10 +19,11 @@ const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
|
19
19
|
const audit_1 = require("../audit");
|
|
20
20
|
const errors_1 = require("../errors");
|
|
21
21
|
__exportStar(require("./dto"), exports);
|
|
22
|
+
const refuseWon = (message) => new errors_1.SupabaseClientError({ status: 400, error: 'Database Error', message });
|
|
22
23
|
// The public contract is the shared StatusEntity from @managemint-solutions/entities; the
|
|
23
24
|
// `as StatusEntity` casts declare the shape the select string below produces (the columns are
|
|
24
25
|
// defined in the api's supabase/migrations).
|
|
25
|
-
const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage';
|
|
26
|
+
const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage, is_won';
|
|
26
27
|
// The audited write returns the whole row; the wire shape is the STATUS_SELECT columns.
|
|
27
28
|
const toStatus = (row) => ({
|
|
28
29
|
mms_id: row.mms_id,
|
|
@@ -31,6 +32,7 @@ const toStatus = (row) => ({
|
|
|
31
32
|
is_complete: row.is_complete,
|
|
32
33
|
is_fail: row.is_fail,
|
|
33
34
|
is_triage: row.is_triage,
|
|
35
|
+
is_won: row.is_won,
|
|
34
36
|
});
|
|
35
37
|
class StatusesResource {
|
|
36
38
|
supabase;
|
|
@@ -66,6 +68,9 @@ class StatusesResource {
|
|
|
66
68
|
return toStatus(row);
|
|
67
69
|
}
|
|
68
70
|
async update(statusId, input) {
|
|
71
|
+
if (await this.isWon(statusId)) {
|
|
72
|
+
throw refuseWon('The Won status is fixed and cannot be changed');
|
|
73
|
+
}
|
|
69
74
|
const row = await this.table.update(statusId, {
|
|
70
75
|
label: input.label,
|
|
71
76
|
colour: input.colour,
|
|
@@ -75,8 +80,28 @@ class StatusesResource {
|
|
|
75
80
|
});
|
|
76
81
|
return toStatus(row);
|
|
77
82
|
}
|
|
83
|
+
/** The Won status cannot be deleted. Every other status can. */
|
|
78
84
|
async remove(statusId) {
|
|
85
|
+
if (await this.isWon(statusId)) {
|
|
86
|
+
throw refuseWon('The Won status is fixed and cannot be deleted');
|
|
87
|
+
}
|
|
79
88
|
await this.table.remove(statusId);
|
|
80
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether the status is this organization's Won lead status (`is_won`), where converting a
|
|
92
|
+
* lead moves it. Won is fixed: it cannot be edited or deleted. Every other status - a lead
|
|
93
|
+
* status the organization marks complete included - is ordinary.
|
|
94
|
+
*/
|
|
95
|
+
async isWon(statusId) {
|
|
96
|
+
const { data, error } = await this.supabase
|
|
97
|
+
.from('statuses')
|
|
98
|
+
.select('is_won')
|
|
99
|
+
.eq('organization_id', this.auth.organization_id)
|
|
100
|
+
.eq('mms_id', statusId)
|
|
101
|
+
.maybeSingle();
|
|
102
|
+
if (error)
|
|
103
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
104
|
+
return !!data?.is_won;
|
|
105
|
+
}
|
|
81
106
|
}
|
|
82
107
|
exports.StatusesResource = StatusesResource;
|
|
@@ -15,9 +15,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.SupportingFilesResource = void 0;
|
|
18
|
-
const enum_1 = require("@managemint-solutions/entities/
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/supporting-files/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
20
|
const audit_1 = require("../audit");
|
|
20
21
|
const errors_1 = require("../errors");
|
|
22
|
+
const won_1 = require("../leads/won");
|
|
21
23
|
const paths_1 = require("./paths");
|
|
22
24
|
__exportStar(require("./dto"), exports);
|
|
23
25
|
__exportStar(require("./paths"), exports);
|
|
@@ -45,7 +47,7 @@ class SupportingFilesResource {
|
|
|
45
47
|
this.auth = auth;
|
|
46
48
|
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
47
49
|
table: 'supporting_files',
|
|
48
|
-
entityType:
|
|
50
|
+
entityType: enum_2.AuditTrailEntityType.SUPPORTING_FILES,
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
async getPresignedUrl(supportingFileId) {
|
|
@@ -77,6 +79,9 @@ class SupportingFilesResource {
|
|
|
77
79
|
return data;
|
|
78
80
|
}
|
|
79
81
|
async upload(entity, entityId, input) {
|
|
82
|
+
if (entity === enum_1.SupportingFilesEntityTypes.LEAD) {
|
|
83
|
+
await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
|
|
84
|
+
}
|
|
80
85
|
const { data: existing, error: existingError } = await this.supabase
|
|
81
86
|
.from('supporting_files')
|
|
82
87
|
.select('name, extension')
|
package/dist/tasks/index.js
CHANGED
|
@@ -37,7 +37,8 @@ const TASK_LIST_SELECT = `
|
|
|
37
37
|
label,
|
|
38
38
|
is_complete,
|
|
39
39
|
is_fail,
|
|
40
|
-
is_triage
|
|
40
|
+
is_triage,
|
|
41
|
+
is_won
|
|
41
42
|
),
|
|
42
43
|
priority,
|
|
43
44
|
labels:labels!task_labels (
|
|
@@ -58,7 +59,7 @@ const LINKED_TASK_FIELDS = `
|
|
|
58
59
|
name,
|
|
59
60
|
priority,
|
|
60
61
|
deleted,
|
|
61
|
-
status:statuses!status_id ( mms_id, colour, label, is_complete, is_fail, is_triage )`;
|
|
62
|
+
status:statuses!status_id ( mms_id, colour, label, is_complete, is_fail, is_triage, is_won )`;
|
|
62
63
|
const TASK_DETAIL_SELECT = `
|
|
63
64
|
mms_id,
|
|
64
65
|
created_at,
|
|
@@ -76,7 +77,8 @@ const TASK_DETAIL_SELECT = `
|
|
|
76
77
|
label,
|
|
77
78
|
is_complete,
|
|
78
79
|
is_fail,
|
|
79
|
-
is_triage
|
|
80
|
+
is_triage,
|
|
81
|
+
is_won
|
|
80
82
|
),
|
|
81
83
|
priority,
|
|
82
84
|
labels:labels!task_labels (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@managemint-solutions/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Typed Supabase data-access SDK for ManageMint Solutions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Scott Bebington <scottbebington@gmail.com>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"testEnvironment": "node"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@managemint-solutions/entities": "^1.
|
|
54
|
+
"@managemint-solutions/entities": "^1.28.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@nestjs/common": "^11.0.0",
|