@managemint-solutions/sdk 0.40.1 → 0.42.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.
@@ -19,6 +19,8 @@ export declare class BillingReadsResource {
19
19
  openTransaction(openStatuses: string[]): Promise<BillingTransactionRow | null>;
20
20
  /** The replay lookup: the same key must never raise a second charge. */
21
21
  transactionByIdempotencyKey(idempotencyKey: string): Promise<BillingTransactionRow | null>;
22
+ /** The organization's newest renewal in this status, or null. */
23
+ latestRenewalIn(transactionStatus: string): Promise<BillingTransactionRow | null>;
22
24
  /**
23
25
  * The organization's payments, newest first: every ledger row, whatever its type or
24
26
  * status. RLS scopes the rows; the eq is belt-and-braces.
@@ -26,7 +26,7 @@ const BILLING_TRANSACTIONS_TABLE = {
26
26
  entityType: enum_1.AuditTrailEntityType.BILLING_TRANSACTIONS,
27
27
  };
28
28
  /** The wire shape of a payment list row - never the internal bigint id, never the checkout secrets. */
29
- const PAYMENT_SUMMARY_SELECT = 'mms_id, created_at, transaction_type, transaction_status, amount_minor, currency, paid_at, failure_reason';
29
+ const PAYMENT_SUMMARY_SELECT = 'mms_id, created_at, transaction_type, transaction_status, amount_minor, currency, paid_at, failure_reason, refunded_at, refund_amount_minor';
30
30
  /**
31
31
  * PaymentEntity, column for column, plus the renewal's invoice through the unique
32
32
  * `invoices.billing_transaction_id` foreign key. The Paystack access code and
@@ -70,6 +70,21 @@ class BillingReadsResource {
70
70
  throw (0, errors_1.mapPostgrestError)(error);
71
71
  return data ? (0, audit_1.withoutInternalId)(data) : null;
72
72
  }
73
+ /** The organization's newest renewal in this status, or null. */
74
+ async latestRenewalIn(transactionStatus) {
75
+ const { data, error } = await this.supabase
76
+ .from('billing_transactions')
77
+ .select('*')
78
+ .eq('organization_id', this.actor.organization_id)
79
+ .eq('transaction_type', enum_2.BillingTransactionType.RENEWAL)
80
+ .eq('transaction_status', transactionStatus)
81
+ .order('created_at', { ascending: false })
82
+ .limit(1)
83
+ .maybeSingle();
84
+ if (error)
85
+ throw (0, errors_1.mapPostgrestError)(error);
86
+ return data ? (0, audit_1.withoutInternalId)(data) : null;
87
+ }
73
88
  /**
74
89
  * The organization's payments, newest first: every ledger row, whatever its type or
75
90
  * status. RLS scopes the rows; the eq is belt-and-braces.
@@ -21,14 +21,26 @@ export declare class InvoicesReadsResource {
21
21
  /**
22
22
  * The document writes, on the service client only - every write policy on `invoices` is
23
23
  * `false` for `authenticated`. Rows are written by the api's billing service beside a
24
- * renewal's ledger write - a renewal is inserted already paid, so an invoice never moves
25
- * status after it is issued - never by a caller.
24
+ * renewal's ledger row, never by a caller; an invoice mirrors its ledger row's status.
26
25
  */
27
26
  export declare class InvoicesResource extends InvoicesReadsResource {
28
27
  private readonly invoices;
29
28
  constructor(supabase: TypedSupabaseClient, actor: AuditActor, audit: AuditResource);
30
29
  /** Audited insert of the accounting document beside its ledger row. */
31
30
  create(payload: AuditRow): Promise<AuditRow>;
31
+ /** Audited status change: the row follows its ledger row (paid, refunded). */
32
+ update(invoiceId: string, patch: AuditRow): Promise<AuditRow>;
33
+ /**
34
+ * The invoice Paystack named with this INV_… code, or null. The invoice events
35
+ * (create, payment_failed, update) all carry the code, so it is how a later event finds
36
+ * the row an earlier one opened.
37
+ */
38
+ byPaystackInvoiceCode(invoiceCode: string): Promise<InvoiceEntity | null>;
39
+ /**
40
+ * The invoice behind a ledger row, or null. `billing_transaction_id` is unique, so a
41
+ * renewal has at most one; every other transaction type has none.
42
+ */
43
+ byTransactionId(transactionId: string): Promise<InvoiceEntity | null>;
32
44
  /**
33
45
  * The organization's highest invoice number, 0 when it has none. The api assigns
34
46
  * `max + 1` and lets the `(organization_id, invoice_number)` unique index arbitrate a
@@ -27,7 +27,7 @@ const INVOICES_TABLE = {
27
27
  /** The wire shape of an invoice list row - never the internal bigint id. */
28
28
  const INVOICE_SUMMARY_SELECT = 'mms_id, invoice_number, transaction_type, status, issued_at, paid_at, amount_minor, currency';
29
29
  /** InvoiceEntity, column for column: the summary plus the frozen document data. */
30
- const INVOICE_ENTITY_SELECT = `${INVOICE_SUMMARY_SELECT}, billing_transaction_id, paystack_reference, period_start, period_end, line_items, billed_to`;
30
+ const INVOICE_ENTITY_SELECT = `${INVOICE_SUMMARY_SELECT}, billing_transaction_id, paystack_reference, paystack_invoice_code, period_start, period_end, line_items, billed_to`;
31
31
  /**
32
32
  * The readable half. `invoices` is SELECT-only for `authenticated` with an org-predicate
33
33
  * policy - the same posture as the ledger it mirrors - so reads run on the caller's own
@@ -78,8 +78,7 @@ exports.InvoicesReadsResource = InvoicesReadsResource;
78
78
  /**
79
79
  * The document writes, on the service client only - every write policy on `invoices` is
80
80
  * `false` for `authenticated`. Rows are written by the api's billing service beside a
81
- * renewal's ledger write - a renewal is inserted already paid, so an invoice never moves
82
- * status after it is issued - never by a caller.
81
+ * renewal's ledger row, never by a caller; an invoice mirrors its ledger row's status.
83
82
  */
84
83
  class InvoicesResource extends InvoicesReadsResource {
85
84
  invoices;
@@ -91,6 +90,44 @@ class InvoicesResource extends InvoicesReadsResource {
91
90
  async create(payload) {
92
91
  return this.invoices.insert(payload);
93
92
  }
93
+ /** Audited status change: the row follows its ledger row (paid, refunded). */
94
+ async update(invoiceId, patch) {
95
+ return this.invoices.update(invoiceId, {
96
+ ...patch,
97
+ updated_at: new Date().toISOString(),
98
+ });
99
+ }
100
+ /**
101
+ * The invoice Paystack named with this INV_… code, or null. The invoice events
102
+ * (create, payment_failed, update) all carry the code, so it is how a later event finds
103
+ * the row an earlier one opened.
104
+ */
105
+ async byPaystackInvoiceCode(invoiceCode) {
106
+ const { data, error } = await this.supabase
107
+ .from('invoices')
108
+ .select(INVOICE_ENTITY_SELECT)
109
+ .eq('paystack_invoice_code', invoiceCode)
110
+ .eq('organization_id', this.actor.organization_id)
111
+ .maybeSingle();
112
+ if (error)
113
+ throw (0, errors_1.mapPostgrestError)(error);
114
+ return data ?? null;
115
+ }
116
+ /**
117
+ * The invoice behind a ledger row, or null. `billing_transaction_id` is unique, so a
118
+ * renewal has at most one; every other transaction type has none.
119
+ */
120
+ async byTransactionId(transactionId) {
121
+ const { data, error } = await this.supabase
122
+ .from('invoices')
123
+ .select(INVOICE_ENTITY_SELECT)
124
+ .eq('billing_transaction_id', transactionId)
125
+ .eq('organization_id', this.actor.organization_id)
126
+ .maybeSingle();
127
+ if (error)
128
+ throw (0, errors_1.mapPostgrestError)(error);
129
+ return data ?? null;
130
+ }
94
131
  /**
95
132
  * The organization's highest invoice number, 0 when it has none. The api assigns
96
133
  * `max + 1` and lets the `(organization_id, invoice_number)` unique index arbitrate a
package/dist/client.d.ts CHANGED
@@ -8,6 +8,7 @@ import { NotesResource } from './notes';
8
8
  import { NotificationsResource } from './notifications';
9
9
  import { BillingReadsResource } from './billing';
10
10
  import { InvoicesReadsResource } from './billing/invoices';
11
+ import { LabelsResource } from './labels';
11
12
  import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from './leave';
12
13
  import { OrganizationsResource } from './organizations';
13
14
  import { PermissionsResource } from './permissions';
@@ -47,6 +48,7 @@ export declare class SupabaseClient {
47
48
  readonly audit: AuditResource;
48
49
  readonly configs: ConfigsResource;
49
50
  readonly statuses: StatusesResource;
51
+ readonly labels: LabelsResource;
50
52
  readonly additionalDetails: AdditionalDetailsResource;
51
53
  readonly notes: NotesResource;
52
54
  readonly notifications: NotificationsResource;
package/dist/client.js CHANGED
@@ -10,6 +10,7 @@ const notes_1 = require("./notes");
10
10
  const notifications_1 = require("./notifications");
11
11
  const billing_1 = require("./billing");
12
12
  const invoices_1 = require("./billing/invoices");
13
+ const labels_1 = require("./labels");
13
14
  const leave_1 = require("./leave");
14
15
  const organizations_1 = require("./organizations");
15
16
  const permissions_1 = require("./permissions");
@@ -32,6 +33,7 @@ class SupabaseClient {
32
33
  // Reads run under the authenticated grant; the writes are the service client's.
33
34
  configs;
34
35
  statuses;
36
+ labels;
35
37
  additionalDetails;
36
38
  notes;
37
39
  notifications;
@@ -58,6 +60,7 @@ class SupabaseClient {
58
60
  this.audit = new audit_1.AuditResource(this.supabase, this.auth, config.onAuditError);
59
61
  this.configs = new configs_1.ConfigsResource(this.supabase);
60
62
  this.statuses = new statuses_1.StatusesResource(this.supabase, this.auth, this.audit);
63
+ this.labels = new labels_1.LabelsResource(this.supabase, this.auth, this.audit);
61
64
  this.additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, this.auth, this.audit);
62
65
  this.notes = new notes_1.NotesResource(this.supabase, this.auth, this.audit);
63
66
  this.notifications = new notifications_1.NotificationsResource(this.supabase, this.auth, this.audit);
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export { CreateFeatureRequestDto } from './feature-requests';
18
18
  export { CreateConfigDto, UpdateConfigDto } from './configs';
19
19
  export { CreateSupportRequestDto } from './support';
20
20
  export { AddStatusDto, GetStatusesDto, StatusIdDto, UpdateStatusDto } from './statuses';
21
+ export { AddLabelDto, GetLabelsDto, LabelIdDto, UpdateLabelDto } from './labels';
21
22
  export { AddAdditionalDetailsDto, AdditionalDetailsIdDto, GetAdditionalDetailsDto, IsAdditionalDetailsValueByType, UpdateAdditionalDetailsDto, } from './additional-details';
22
23
  export { AddNoteDto, GetNotesDto, NoteIdDto } from './notes';
23
24
  export type { CreateNotificationInput, CreatedNotification, NotificationContentRow, OrganizationTableFactory, QueuedNotification, } from './notifications';
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  // their classes are not exported: a consumer never constructs one, and every export is a contract
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
- exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = 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.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.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = 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.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = void 0;
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.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = 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.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = void 0;
12
12
  var client_1 = require("./client");
13
13
  Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
14
14
  Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
@@ -53,6 +53,11 @@ Object.defineProperty(exports, "AddStatusDto", { enumerable: true, get: function
53
53
  Object.defineProperty(exports, "GetStatusesDto", { enumerable: true, get: function () { return statuses_1.GetStatusesDto; } });
54
54
  Object.defineProperty(exports, "StatusIdDto", { enumerable: true, get: function () { return statuses_1.StatusIdDto; } });
55
55
  Object.defineProperty(exports, "UpdateStatusDto", { enumerable: true, get: function () { return statuses_1.UpdateStatusDto; } });
56
+ var labels_1 = require("./labels");
57
+ Object.defineProperty(exports, "AddLabelDto", { enumerable: true, get: function () { return labels_1.AddLabelDto; } });
58
+ Object.defineProperty(exports, "GetLabelsDto", { enumerable: true, get: function () { return labels_1.GetLabelsDto; } });
59
+ Object.defineProperty(exports, "LabelIdDto", { enumerable: true, get: function () { return labels_1.LabelIdDto; } });
60
+ Object.defineProperty(exports, "UpdateLabelDto", { enumerable: true, get: function () { return labels_1.UpdateLabelDto; } });
56
61
  var additional_details_1 = require("./additional-details");
57
62
  Object.defineProperty(exports, "AddAdditionalDetailsDto", { enumerable: true, get: function () { return additional_details_1.AddAdditionalDetailsDto; } });
58
63
  Object.defineProperty(exports, "AdditionalDetailsIdDto", { enumerable: true, get: function () { return additional_details_1.AdditionalDetailsIdDto; } });
@@ -0,0 +1,19 @@
1
+ import type { AddLabelDto as AddLabelDtoType, GetLabelsDto as GetLabelsDtoType, LabelIdDto as LabelIdDtoType, UpdateLabelDto as UpdateLabelDtoType } from '@managemint-solutions/entities/labels/dto';
2
+ import { LabelEntityTypes } from '@managemint-solutions/entities/labels/enum';
3
+ export declare class LabelIdDto implements LabelIdDtoType {
4
+ readonly labelId: string;
5
+ }
6
+ export declare class GetLabelsDto implements GetLabelsDtoType {
7
+ readonly entity: LabelEntityTypes;
8
+ }
9
+ export declare class AddLabelDto implements AddLabelDtoType {
10
+ readonly name: string;
11
+ readonly colour: string;
12
+ readonly entity: LabelEntityTypes;
13
+ readonly description?: string | null;
14
+ }
15
+ export declare class UpdateLabelDto implements UpdateLabelDtoType {
16
+ readonly name: string;
17
+ readonly colour: string;
18
+ readonly description?: string | null;
19
+ }
@@ -0,0 +1,95 @@
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.UpdateLabelDto = exports.AddLabelDto = exports.GetLabelsDto = exports.LabelIdDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const enum_1 = require("@managemint-solutions/entities/labels/enum");
15
+ const validators_1 = require("../validators");
16
+ // class-validator versions of the shared DTO types from @managemint-solutions/entities.
17
+ // Each class `implements` its entities type so the wire contract cannot drift.
18
+ class LabelIdDto {
19
+ labelId;
20
+ }
21
+ exports.LabelIdDto = LabelIdDto;
22
+ __decorate([
23
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid label ID format' }),
24
+ __metadata("design:type", String)
25
+ ], LabelIdDto.prototype, "labelId", void 0);
26
+ class GetLabelsDto {
27
+ entity;
28
+ }
29
+ exports.GetLabelsDto = GetLabelsDto;
30
+ __decorate([
31
+ (0, class_validator_1.IsEnum)(enum_1.LabelEntityTypes, { message: 'Invalid entity type' }),
32
+ __metadata("design:type", String)
33
+ ], GetLabelsDto.prototype, "entity", void 0);
34
+ class AddLabelDto {
35
+ name;
36
+ // Colour must be a valid hex code (e.g., #FFFFFF)
37
+ colour;
38
+ // `entity` is the wire name; the SDK maps it to the `entity_type` column.
39
+ entity;
40
+ // What the label is for; omitted or null means none.
41
+ description;
42
+ }
43
+ exports.AddLabelDto = AddLabelDto;
44
+ __decorate([
45
+ (0, class_validator_1.IsString)({ message: 'Invalid name' }),
46
+ (0, class_validator_1.MinLength)(1, { message: 'Name cannot be empty' }),
47
+ (0, class_validator_1.MaxLength)(50, { message: 'Name cannot exceed 50 characters' }),
48
+ __metadata("design:type", String)
49
+ ], AddLabelDto.prototype, "name", void 0);
50
+ __decorate([
51
+ (0, class_validator_1.IsString)({ message: 'Invalid colour format' }),
52
+ (0, class_validator_1.MinLength)(7, { message: 'Colour must be a valid hex code' }),
53
+ (0, class_validator_1.MaxLength)(7, { message: 'Colour must be a valid hex code' }),
54
+ (0, class_validator_1.Matches)(/^#[0-9A-Fa-f]{6}$/, { message: 'Colour must be a valid hex code' }),
55
+ __metadata("design:type", String)
56
+ ], AddLabelDto.prototype, "colour", void 0);
57
+ __decorate([
58
+ (0, class_validator_1.IsEnum)(enum_1.LabelEntityTypes, { message: 'Invalid entity type' }),
59
+ __metadata("design:type", String)
60
+ ], AddLabelDto.prototype, "entity", void 0);
61
+ __decorate([
62
+ (0, class_validator_1.IsOptional)(),
63
+ (0, validators_1.IsNullable)(),
64
+ (0, class_validator_1.IsString)({ message: 'Invalid description' }),
65
+ (0, class_validator_1.MaxLength)(255, { message: 'Description cannot exceed 255 characters' }),
66
+ __metadata("design:type", Object)
67
+ ], AddLabelDto.prototype, "description", void 0);
68
+ class UpdateLabelDto {
69
+ name;
70
+ // Colour must be a valid hex code (e.g., #FFFFFF)
71
+ colour;
72
+ // Omitted leaves the description as it is; null clears it.
73
+ description;
74
+ }
75
+ exports.UpdateLabelDto = UpdateLabelDto;
76
+ __decorate([
77
+ (0, class_validator_1.IsString)({ message: 'Invalid name' }),
78
+ (0, class_validator_1.MinLength)(1, { message: 'Name cannot be empty' }),
79
+ (0, class_validator_1.MaxLength)(50, { message: 'Name cannot exceed 50 characters' }),
80
+ __metadata("design:type", String)
81
+ ], UpdateLabelDto.prototype, "name", void 0);
82
+ __decorate([
83
+ (0, class_validator_1.IsString)({ message: 'Invalid colour format' }),
84
+ (0, class_validator_1.MinLength)(7, { message: 'Colour must be a valid hex code' }),
85
+ (0, class_validator_1.MaxLength)(7, { message: 'Colour must be a valid hex code' }),
86
+ (0, class_validator_1.Matches)(/^#[0-9A-Fa-f]{6}$/, { message: 'Colour must be a valid hex code' }),
87
+ __metadata("design:type", String)
88
+ ], UpdateLabelDto.prototype, "colour", void 0);
89
+ __decorate([
90
+ (0, class_validator_1.IsOptional)(),
91
+ (0, validators_1.IsNullable)(),
92
+ (0, class_validator_1.IsString)({ message: 'Invalid description' }),
93
+ (0, class_validator_1.MaxLength)(255, { message: 'Description cannot exceed 255 characters' }),
94
+ __metadata("design:type", Object)
95
+ ], UpdateLabelDto.prototype, "description", void 0);
@@ -0,0 +1,17 @@
1
+ import type { LabelEntity } from '@managemint-solutions/entities/labels';
2
+ import type { AddLabelDto, UpdateLabelDto } from '@managemint-solutions/entities/labels/dto';
3
+ import { LabelEntityTypes } from '@managemint-solutions/entities/labels/enum';
4
+ import type { AuthContext } from '../auth';
5
+ import { type AuditResource } from '../audit';
6
+ import type { TypedSupabaseClient } from '../client';
7
+ export * from './dto';
8
+ export declare class LabelsResource {
9
+ private readonly supabase;
10
+ private readonly auth;
11
+ private readonly table;
12
+ constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
+ list(entity: LabelEntityTypes): Promise<LabelEntity[]>;
14
+ create(input: AddLabelDto): Promise<LabelEntity>;
15
+ update(labelId: string, input: UpdateLabelDto): Promise<LabelEntity>;
16
+ remove(labelId: string): Promise<void>;
17
+ }
@@ -0,0 +1,88 @@
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.LabelsResource = void 0;
18
+ const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
+ const audit_1 = require("../audit");
20
+ const errors_1 = require("../errors");
21
+ __exportStar(require("./dto"), exports);
22
+ // The public contract is the shared LabelEntity from @managemint-solutions/entities; the
23
+ // `as LabelEntity` casts declare the shape the select string below produces (the columns are
24
+ // defined in the api's supabase/migrations).
25
+ const LABEL_SELECT = 'mms_id, name, colour, description';
26
+ // The audited write returns the whole row; the wire shape is the LABEL_SELECT columns.
27
+ const toLabel = (row) => ({
28
+ mms_id: row.mms_id,
29
+ name: row.name,
30
+ colour: row.colour,
31
+ description: row.description,
32
+ });
33
+ // The unique index is (organization, entity type, lower(name)), so a 23505 from a label write can
34
+ // only mean the name is taken. Postgres names the index; the caller gets a sentence instead.
35
+ const refuseDuplicateName = (error) => {
36
+ if (error instanceof errors_1.SupabaseClientError && error.code === '23505') {
37
+ throw new errors_1.SupabaseClientError({ status: 409, error: error.error, message: 'A label with that name already exists', code: error.code }, { cause: error });
38
+ }
39
+ throw error;
40
+ };
41
+ class LabelsResource {
42
+ supabase;
43
+ auth;
44
+ table;
45
+ constructor(supabase, auth, audit) {
46
+ this.supabase = supabase;
47
+ this.auth = auth;
48
+ this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
49
+ table: 'labels',
50
+ entityType: enum_1.AuditTrailEntityType.LABEL,
51
+ });
52
+ }
53
+ async list(entity) {
54
+ const { data, error } = await this.supabase
55
+ .from('labels')
56
+ .select(LABEL_SELECT)
57
+ .eq('organization_id', this.auth.organization_id)
58
+ .eq('entity_type', entity);
59
+ if (error)
60
+ throw (0, errors_1.mapPostgrestError)(error);
61
+ return data;
62
+ }
63
+ async create(input) {
64
+ const row = await this.table.insert({
65
+ name: input.name,
66
+ colour: input.colour,
67
+ entity_type: input.entity,
68
+ description: input.description ?? null,
69
+ organization_id: this.auth.organization_id,
70
+ created_by: this.auth.mms_id,
71
+ }).catch(refuseDuplicateName);
72
+ return toLabel(row);
73
+ }
74
+ async update(labelId, input) {
75
+ const row = await this.table.update(labelId, {
76
+ name: input.name,
77
+ colour: input.colour,
78
+ ...(input.description !== undefined && { description: input.description }),
79
+ updated_at: new Date().toISOString(),
80
+ last_updated_by: this.auth.mms_id,
81
+ }).catch(refuseDuplicateName);
82
+ return toLabel(row);
83
+ }
84
+ async remove(labelId) {
85
+ await this.table.remove(labelId);
86
+ }
87
+ }
88
+ exports.LabelsResource = LabelsResource;
@@ -103,5 +103,12 @@ export declare class SupabaseServiceClient {
103
103
  * transaction id in its metadata, and the row is what says which tenant it belongs to.
104
104
  */
105
105
  findTransaction(transactionId: string): Promise<Record<string, unknown> | null>;
106
+ /**
107
+ * A ledger row by its Paystack reference. Unscoped for the same reason as
108
+ * `findTransaction`: a refund event names only `transaction_reference`, and the row is
109
+ * what says which tenant it belongs to. The partial unique index on
110
+ * `paystack_reference` makes at most one row match.
111
+ */
112
+ findTransactionByReference(reference: string): Promise<Record<string, unknown> | null>;
106
113
  }
107
114
  export declare const createSupabaseServiceClient: (config: SupabaseServiceClientConfig) => SupabaseServiceClient;
@@ -182,6 +182,22 @@ class SupabaseServiceClient {
182
182
  throw (0, errors_1.mapPostgrestError)(error);
183
183
  return data ? (0, audit_1.withoutInternalId)(data) : null;
184
184
  }
185
+ /**
186
+ * A ledger row by its Paystack reference. Unscoped for the same reason as
187
+ * `findTransaction`: a refund event names only `transaction_reference`, and the row is
188
+ * what says which tenant it belongs to. The partial unique index on
189
+ * `paystack_reference` makes at most one row match.
190
+ */
191
+ async findTransactionByReference(reference) {
192
+ const { data, error } = await this.supabase
193
+ .from('billing_transactions')
194
+ .select('*')
195
+ .eq('paystack_reference', reference)
196
+ .maybeSingle();
197
+ if (error)
198
+ throw (0, errors_1.mapPostgrestError)(error);
199
+ return data ? (0, audit_1.withoutInternalId)(data) : null;
200
+ }
185
201
  }
186
202
  exports.SupabaseServiceClient = SupabaseServiceClient;
187
203
  const createSupabaseServiceClient = (config) => new SupabaseServiceClient(config);
@@ -2,7 +2,7 @@ import type { CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType
2
2
  export declare class TaskIdDto implements TaskIdDtoType {
3
3
  readonly taskId: string;
4
4
  }
5
- export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in' | 'status_id_not_in' | 'priority_in'> {
5
+ export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in' | 'status_id_not_in' | 'label_id_in' | 'priority_in'> {
6
6
  readonly search?: string | null;
7
7
  readonly page: number;
8
8
  readonly page_size: number;
@@ -17,6 +17,7 @@ export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in'
17
17
  readonly end_date_before?: string | null;
18
18
  readonly status_id_in?: string[] | null;
19
19
  readonly status_id_not_in?: string[] | null;
20
+ readonly label_id_in?: string[] | null;
20
21
  readonly completed?: boolean | null;
21
22
  readonly priority_in?: number[] | null;
22
23
  readonly archive: boolean;
@@ -31,6 +32,7 @@ export declare class CreateTaskDto implements CreateTaskDtoType {
31
32
  readonly assigned_to_id?: string | null;
32
33
  readonly project_id?: string | null;
33
34
  readonly client_id?: string | null;
35
+ readonly label_ids?: string[] | null;
34
36
  }
35
37
  export declare class UpdateTaskDto implements UpdateTaskDtoType {
36
38
  readonly name: string;
@@ -42,4 +44,5 @@ export declare class UpdateTaskDto implements UpdateTaskDtoType {
42
44
  readonly assigned_to_id?: string | null;
43
45
  readonly project_id?: string | null;
44
46
  readonly client_id?: string | null;
47
+ readonly label_ids?: string[] | null;
45
48
  }
package/dist/tasks/dto.js CHANGED
@@ -25,10 +25,11 @@ __decorate([
25
25
  ,
26
26
  __metadata("design:type", String)
27
27
  ], TaskIdDto.prototype, "taskId", void 0);
28
- // `status_id_in`, `status_id_not_in` and `priority_in` are omitted from the implemented type on
29
- // purpose: the entities type carries the CSV wire values, while this class carries the
30
- // `@Transform`-parsed arrays the query builder feeds to `.in('status_id', ...)` /
31
- // `.in('priority', ...)` and to the `status_id.is.null,status_id.not.in.(...)` exclusion.
28
+ // `status_id_in`, `status_id_not_in`, `label_id_in` and `priority_in` are omitted from the
29
+ // implemented type on purpose: the entities type carries the CSV wire values, while this class
30
+ // carries the `@Transform`-parsed arrays the query builder feeds to `.in('status_id', ...)` /
31
+ // `.in('task_labels.label_id', ...)` / `.in('priority', ...)` and to the
32
+ // `status_id.is.null,status_id.not.in.(...)` exclusion.
32
33
  class GetTasksDto {
33
34
  search;
34
35
  page = 1;
@@ -44,6 +45,8 @@ class GetTasksDto {
44
45
  end_date_before;
45
46
  status_id_in;
46
47
  status_id_not_in;
48
+ // A task matches when it carries any one of these labels.
49
+ label_id_in;
47
50
  // Tri-state: true = only completed rows, false = only rows that are not completed, omitted = no
48
51
  // completion filter. The api resolves it to status ids before it reaches `list`.
49
52
  completed;
@@ -147,6 +150,13 @@ __decorate([
147
150
  (0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid status ID format' }),
148
151
  __metadata("design:type", Object)
149
152
  ], GetTasksDto.prototype, "status_id_not_in", void 0);
153
+ __decorate([
154
+ (0, class_validator_1.IsOptional)(),
155
+ (0, validators_1.IsNullable)(),
156
+ (0, class_transformer_1.Transform)(({ value }) => value ? value.split(',').map((id) => id.trim()) : null, { toClassOnly: true }),
157
+ (0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid label ID format' }),
158
+ __metadata("design:type", Object)
159
+ ], GetTasksDto.prototype, "label_id_in", void 0);
150
160
  __decorate([
151
161
  (0, class_validator_1.IsOptional)(),
152
162
  (0, validators_1.IsNullable)(),
@@ -178,6 +188,8 @@ class CreateTaskDto {
178
188
  assigned_to_id;
179
189
  project_id;
180
190
  client_id;
191
+ // The labels to pin to the new task; omitted means none.
192
+ label_ids;
181
193
  }
182
194
  exports.CreateTaskDto = CreateTaskDto;
183
195
  __decorate([
@@ -239,6 +251,13 @@ __decorate([
239
251
  (0, class_validator_1.IsUUID)('7', { message: 'Invalid client ID format' }),
240
252
  __metadata("design:type", Object)
241
253
  ], CreateTaskDto.prototype, "client_id", void 0);
254
+ __decorate([
255
+ (0, class_validator_1.IsOptional)(),
256
+ (0, class_validator_1.IsArray)({ message: 'Label IDs must be an array' }),
257
+ (0, class_validator_1.ArrayUnique)({ message: 'Label IDs must not repeat' }),
258
+ (0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid label ID format' }),
259
+ __metadata("design:type", Object)
260
+ ], CreateTaskDto.prototype, "label_ids", void 0);
242
261
  class UpdateTaskDto {
243
262
  name;
244
263
  description;
@@ -249,6 +268,9 @@ class UpdateTaskDto {
249
268
  assigned_to_id;
250
269
  project_id;
251
270
  client_id;
271
+ // The full set of labels after the update: an array replaces the set (empty clears it);
272
+ // omitted or null leaves the labels as they are.
273
+ label_ids;
252
274
  }
253
275
  exports.UpdateTaskDto = UpdateTaskDto;
254
276
  __decorate([
@@ -312,3 +334,10 @@ __decorate([
312
334
  (0, class_validator_1.IsUUID)('7', { message: 'Invalid client ID format' }),
313
335
  __metadata("design:type", Object)
314
336
  ], UpdateTaskDto.prototype, "client_id", void 0);
337
+ __decorate([
338
+ (0, class_validator_1.IsOptional)(),
339
+ (0, class_validator_1.IsArray)({ message: 'Label IDs must be an array' }),
340
+ (0, class_validator_1.ArrayUnique)({ message: 'Label IDs must not repeat' }),
341
+ (0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid label ID format' }),
342
+ __metadata("design:type", Object)
343
+ ], UpdateTaskDto.prototype, "label_ids", void 0);
@@ -8,6 +8,7 @@ export * from './dto';
8
8
  export declare class TasksResource {
9
9
  private readonly supabase;
10
10
  private readonly auth;
11
+ private readonly audit;
11
12
  private readonly table;
12
13
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
14
  list(query: GetTasksDto): Promise<MmsList<TaskSummary>>;
@@ -19,4 +20,20 @@ export declare class TasksResource {
19
20
  restore(taskId: string): Promise<TaskEntity>;
20
21
  /** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
21
22
  remove(taskId: string): Promise<void>;
23
+ /**
24
+ * Refuses, before anything is written, a label id that is not one of this organization's task
25
+ * labels - one deleted while the form was open, or another organization's. The `task_labels`
26
+ * trigger enforces the same rule, but only once the task row has already been saved, so
27
+ * without this a create would answer an error for a task it had in fact created.
28
+ */
29
+ private checkLabels;
30
+ /**
31
+ * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
32
+ * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
33
+ * only when the set actually moved - on a new task that entry is the record of the initial
34
+ * pinning. Only the difference is written, the new labels inserted before the dropped ones are
35
+ * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
36
+ * task's existing labels are untouched, and a label kept across an edit keeps its place.
37
+ */
38
+ private setLabels;
22
39
  }
@@ -15,7 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.TasksResource = void 0;
18
- const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
18
+ const enum_1 = require("@managemint-solutions/entities/labels/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");
21
22
  const transforms_1 = require("../transforms");
@@ -38,6 +39,12 @@ const TASK_LIST_SELECT = `
38
39
  is_triage
39
40
  ),
40
41
  priority,
42
+ labels:labels!task_labels (
43
+ mms_id,
44
+ name,
45
+ colour,
46
+ description
47
+ ),
41
48
  assigned_to:user_profiles!assigned_to_id (*),
42
49
  project:project_profiles!project_id (*),
43
50
  client:client_profiles!client_id (*)
@@ -62,6 +69,12 @@ const TASK_DETAIL_SELECT = `
62
69
  is_triage
63
70
  ),
64
71
  priority,
72
+ labels:labels!task_labels (
73
+ mms_id,
74
+ name,
75
+ colour,
76
+ description
77
+ ),
65
78
  assigned_to:user_profiles!assigned_to_id (*),
66
79
  project:project_profiles!project_id (*),
67
80
  client:client_profiles!client_id (*),
@@ -76,16 +89,24 @@ const TASK_DETAIL_SELECT = `
76
89
  restored_at,
77
90
  restored_by:user_profiles!restored_by (*)
78
91
  `;
92
+ // Added to the list select only when `label_id_in` is set: an inner join on the join table is how
93
+ // PostgREST filters a task by the labels it carries.
94
+ const LABEL_FILTER_SELECT = 'task_labels!inner ( label_id )';
95
+ // A `task_labels` row as `setLabels` reads and writes it: the label id, and the name the audit
96
+ // entry records.
97
+ const PINNED_LABEL_SELECT = 'label_id, label:labels ( name )';
79
98
  class TasksResource {
80
99
  supabase;
81
100
  auth;
101
+ audit;
82
102
  table;
83
103
  constructor(supabase, auth, audit) {
84
104
  this.supabase = supabase;
85
105
  this.auth = auth;
106
+ this.audit = audit;
86
107
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
87
108
  table: 'tasks',
88
- entityType: enum_1.AuditTrailEntityType.TASKS,
109
+ entityType: enum_2.AuditTrailEntityType.TASKS,
89
110
  });
90
111
  }
91
112
  async list(query) {
@@ -93,11 +114,14 @@ class TasksResource {
93
114
  const pageSize = query.page_size;
94
115
  const offset = (page - 1) * pageSize;
95
116
  const trimmedSearch = query.search?.trim();
117
+ const labelIdIn = query.label_id_in && query.label_id_in.length > 0 ? query.label_id_in : null;
96
118
  // `order`/`range` return `this`, so the builder stays a filter builder and the conditional
97
119
  // filters below can be reassigned onto it without a cast.
98
120
  let tasksQuery = this.supabase
99
121
  .from('tasks')
100
- .select(TASK_LIST_SELECT, { count: 'estimated' })
122
+ .select(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, {
123
+ count: 'estimated',
124
+ })
101
125
  .eq('organization_id', this.auth.organization_id)
102
126
  .eq('archived', query.archive)
103
127
  .eq('deleted', false)
@@ -152,10 +176,19 @@ class TasksResource {
152
176
  if (query.priority_in && query.priority_in.length > 0) {
153
177
  tasksQuery = tasksQuery.in('priority', query.priority_in);
154
178
  }
179
+ // A task matches when it carries any of the labels. The `!inner` join on `task_labels` only
180
+ // narrows the rows - the `labels` embed is a separate join, so a matched task still shows
181
+ // every label it carries. The join's rows are not part of TaskSummary and are dropped below.
182
+ if (labelIdIn) {
183
+ tasksQuery = tasksQuery.in('task_labels.label_id', labelIdIn);
184
+ }
155
185
  const { data, error, count } = await tasksQuery;
156
186
  if (error)
157
187
  throw (0, errors_1.mapPostgrestError)(error);
158
- return (0, transforms_1.toMmsList)(data, page, pageSize, count);
188
+ const rows = labelIdIn
189
+ ? data.map(({ task_labels: _filterRows, ...task }) => task)
190
+ : data;
191
+ return (0, transforms_1.toMmsList)(rows, page, pageSize, count);
159
192
  }
160
193
  async getById(taskId) {
161
194
  const { data, error } = await this.supabase
@@ -169,39 +202,140 @@ class TasksResource {
169
202
  return data;
170
203
  }
171
204
  async create(input) {
205
+ // `label_ids` is not a tasks column: the labels are join rows, written after the task.
206
+ const { label_ids: labelIds, ...fields } = input;
207
+ if (labelIds) {
208
+ await this.checkLabels(labelIds);
209
+ }
172
210
  // `task_id` (the per-organization task number) is left out: the table's `set_task_id`
173
211
  // BEFORE INSERT trigger fills it in when it arrives null.
174
212
  const row = await this.table.insert({
175
- ...input,
213
+ ...fields,
176
214
  organization_id: this.auth.organization_id,
177
215
  created_by: this.auth.mms_id,
178
216
  created_at: new Date().toISOString(),
179
217
  });
218
+ if (labelIds) {
219
+ await this.setLabels(row.mms_id, labelIds);
220
+ }
180
221
  return this.getById(row.mms_id);
181
222
  }
182
223
  async update(taskId, input) {
224
+ // `label_ids` is not a tasks column; omitted or null leaves the labels as they are.
225
+ const { label_ids: labelIds, ...fields } = input;
226
+ if (labelIds) {
227
+ await this.checkLabels(labelIds);
228
+ }
183
229
  await this.table.update(taskId, {
184
- ...input,
230
+ ...fields,
185
231
  last_updated_by: this.auth.mms_id,
186
232
  updated_at: new Date().toISOString(),
187
233
  });
234
+ if (labelIds) {
235
+ await this.setLabels(taskId, labelIds);
236
+ }
188
237
  return this.getById(taskId);
189
238
  }
190
239
  async archive(taskId) {
191
- await this.table.transition(taskId, enum_1.AuditTrailAction.ARCHIVE, (0, transforms_1.archivedPatch)(this.auth.mms_id));
240
+ await this.table.transition(taskId, enum_2.AuditTrailAction.ARCHIVE, (0, transforms_1.archivedPatch)(this.auth.mms_id));
192
241
  return this.getById(taskId);
193
242
  }
194
243
  async unarchive(taskId) {
195
- await this.table.transition(taskId, enum_1.AuditTrailAction.UNARCHIVE, (0, transforms_1.unarchivedPatch)(this.auth.mms_id));
244
+ await this.table.transition(taskId, enum_2.AuditTrailAction.UNARCHIVE, (0, transforms_1.unarchivedPatch)(this.auth.mms_id));
196
245
  return this.getById(taskId);
197
246
  }
198
247
  async restore(taskId) {
199
- await this.table.transition(taskId, enum_1.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
248
+ await this.table.transition(taskId, enum_2.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
200
249
  return this.getById(taskId);
201
250
  }
202
251
  /** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
203
252
  async remove(taskId) {
204
- await this.table.transition(taskId, enum_1.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
253
+ await this.table.transition(taskId, enum_2.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
254
+ }
255
+ /**
256
+ * Refuses, before anything is written, a label id that is not one of this organization's task
257
+ * labels - one deleted while the form was open, or another organization's. The `task_labels`
258
+ * trigger enforces the same rule, but only once the task row has already been saved, so
259
+ * without this a create would answer an error for a task it had in fact created.
260
+ */
261
+ async checkLabels(labelIds) {
262
+ if (labelIds.length === 0)
263
+ return;
264
+ const { count, error } = await this.supabase
265
+ .from('labels')
266
+ .select('mms_id', { count: 'exact', head: true })
267
+ .eq('organization_id', this.auth.organization_id)
268
+ .eq('entity_type', enum_1.LabelEntityTypes.TASK)
269
+ .in('mms_id', labelIds);
270
+ if (error)
271
+ throw (0, errors_1.mapPostgrestError)(error);
272
+ if (count !== labelIds.length) {
273
+ throw new errors_1.SupabaseClientError({
274
+ status: 400,
275
+ error: 'Database Error',
276
+ message: 'Invalid label_id for this organization',
277
+ });
278
+ }
279
+ }
280
+ /**
281
+ * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
282
+ * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
283
+ * only when the set actually moved - on a new task that entry is the record of the initial
284
+ * pinning. Only the difference is written, the new labels inserted before the dropped ones are
285
+ * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
286
+ * task's existing labels are untouched, and a label kept across an edit keeps its place.
287
+ */
288
+ async setLabels(taskId, labelIds) {
289
+ const { data: current, error: readError } = await this.supabase
290
+ .from('task_labels')
291
+ .select(PINNED_LABEL_SELECT)
292
+ .eq('task_id', taskId)
293
+ .eq('organization_id', this.auth.organization_id)
294
+ .order('created_at');
295
+ if (readError)
296
+ throw (0, errors_1.mapPostgrestError)(readError);
297
+ const pinned = current;
298
+ const added = labelIds.filter((labelId) => !pinned.some((row) => row.label_id === labelId));
299
+ const kept = pinned.filter((row) => labelIds.includes(row.label_id));
300
+ const dropped = pinned.filter((row) => !labelIds.includes(row.label_id));
301
+ if (added.length === 0 && dropped.length === 0)
302
+ return;
303
+ let addedRows = [];
304
+ if (added.length > 0) {
305
+ const { data, error } = await this.supabase
306
+ .from('task_labels')
307
+ .insert(added.map((labelId) => ({
308
+ task_id: taskId,
309
+ label_id: labelId,
310
+ organization_id: this.auth.organization_id,
311
+ created_by: this.auth.mms_id,
312
+ })))
313
+ .select(PINNED_LABEL_SELECT);
314
+ if (error)
315
+ throw (0, errors_1.mapPostgrestError)(error);
316
+ addedRows = data;
317
+ }
318
+ if (dropped.length > 0) {
319
+ const { error } = await this.supabase
320
+ .from('task_labels')
321
+ .delete()
322
+ .eq('task_id', taskId)
323
+ .eq('organization_id', this.auth.organization_id)
324
+ .in('label_id', dropped.map((row) => row.label_id));
325
+ if (error)
326
+ throw (0, errors_1.mapPostgrestError)(error);
327
+ }
328
+ await this.audit.record({
329
+ action: enum_2.AuditTrailAction.UPDATE,
330
+ entityType: enum_2.AuditTrailEntityType.TASKS,
331
+ entityId: taskId,
332
+ changes: {
333
+ labels: {
334
+ old: pinned.map((row) => row.label.name),
335
+ new: [...kept, ...addedRows].map((row) => row.label.name),
336
+ },
337
+ },
338
+ });
205
339
  }
206
340
  }
207
341
  exports.TasksResource = TasksResource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@managemint-solutions/sdk",
3
- "version": "0.40.1",
3
+ "version": "0.42.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.23.1"
54
+ "@managemint-solutions/entities": "^1.25.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@nestjs/common": "^11.0.0",