@oneuptime/common 7.0.5065 → 7.0.5069

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.
Files changed (33) hide show
  1. package/Models/DatabaseModels/Project.ts +28 -1
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.ts +11 -10
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.ts +23 -12
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.ts +17 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +3 -1
  6. package/Server/Services/BillingService.ts +30 -8
  7. package/Server/Services/ProjectService.ts +11 -4
  8. package/Server/Services/StatusPageService.ts +19 -3
  9. package/Server/Types/Markdown.ts +17 -17
  10. package/UI/Components/Detail/Detail.tsx +2 -3
  11. package/UI/Utils/Countries.ts +251 -248
  12. package/build/dist/Models/DatabaseModels/Project.js +27 -0
  13. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  14. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.js +1 -1
  15. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.js +1 -1
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.js.map +1 -1
  18. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.js +12 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.js.map +1 -0
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +3 -1
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  22. package/build/dist/Server/Services/BillingService.js +27 -10
  23. package/build/dist/Server/Services/BillingService.js.map +1 -1
  24. package/build/dist/Server/Services/ProjectService.js +8 -3
  25. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  26. package/build/dist/Server/Services/StatusPageService.js +11 -2
  27. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  28. package/build/dist/Server/Types/Markdown.js +16 -16
  29. package/build/dist/UI/Components/Detail/Detail.js +2 -1
  30. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  31. package/build/dist/UI/Utils/Countries.js +247 -247
  32. package/build/dist/UI/Utils/Countries.js.map +1 -1
  33. package/package.json +1 -1
@@ -289,7 +289,8 @@ export default class Project extends TenantModel {
289
289
  @TableColumn({
290
290
  type: TableColumnType.ShortText,
291
291
  title: "Business Country (ISO Alpha-2)",
292
- description: "Two-letter ISO country code for billing address (e.g., US, GB, DE).",
292
+ description:
293
+ "Two-letter ISO country code for billing address (e.g., US, GB, DE).",
293
294
  })
294
295
  @Column({
295
296
  type: ColumnType.ShortText,
@@ -299,6 +300,32 @@ export default class Project extends TenantModel {
299
300
  })
300
301
  public businessDetailsCountry?: string = undefined;
301
302
 
303
+ @ColumnAccessControl({
304
+ create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
305
+ read: [
306
+ Permission.ProjectOwner,
307
+ Permission.ProjectAdmin,
308
+ Permission.ProjectMember,
309
+ Permission.ReadProject,
310
+ Permission.UnAuthorizedSsoUser,
311
+ Permission.ProjectUser,
312
+ ],
313
+ update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
314
+ })
315
+ @TableColumn({
316
+ type: TableColumnType.Email,
317
+ title: "Finance / Accounting Email",
318
+ description:
319
+ "Invoices, receipts and billing related notifications will be sent to this email in addition to project owner.",
320
+ })
321
+ @Column({
322
+ type: ColumnType.Email,
323
+ length: ColumnLength.Email,
324
+ nullable: true,
325
+ unique: false,
326
+ })
327
+ public financeAccountingEmail?: string = undefined;
328
+
302
329
  @ColumnAccessControl({
303
330
  create: [],
304
331
  read: [
@@ -1,16 +1,17 @@
1
1
  import { MigrationInterface, QueryRunner } from "typeorm";
2
2
 
3
3
  export class MigrationName1756293325324 implements MigrationInterface {
4
- public name = 'MigrationName1756293325324'
4
+ public name = "MigrationName1756293325324";
5
5
 
6
- public async up(queryRunner: QueryRunner): Promise<void> {
7
- await queryRunner.query(`ALTER TABLE "Project" ADD "businessDetails" character varying(500)`);
8
- }
9
-
10
- public async down(queryRunner: QueryRunner): Promise<void> {
11
-
12
- await queryRunner.query(`ALTER TABLE "Project" DROP COLUMN "businessDetails"`);
13
-
14
- }
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Project" ADD "businessDetails" character varying(500)`,
9
+ );
10
+ }
15
11
 
12
+ public async down(queryRunner: QueryRunner): Promise<void> {
13
+ await queryRunner.query(
14
+ `ALTER TABLE "Project" DROP COLUMN "businessDetails"`,
15
+ );
16
+ }
16
17
  }
@@ -1,18 +1,29 @@
1
1
  import { MigrationInterface, QueryRunner } from "typeorm";
2
2
 
3
3
  export class MigrationName1756296282627 implements MigrationInterface {
4
- public name = 'MigrationName1756296282627'
4
+ public name = "MigrationName1756296282627";
5
5
 
6
- public async up(queryRunner: QueryRunner): Promise<void> {
7
- await queryRunner.query(`ALTER TABLE "Project" ADD "businessDetailsCountry" character varying(100)`);
8
- await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
9
- await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
10
- }
11
-
12
- public async down(queryRunner: QueryRunner): Promise<void> {
13
- await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
14
- await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
15
- await queryRunner.query(`ALTER TABLE "Project" DROP COLUMN "businessDetailsCountry"`);
16
- }
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Project" ADD "businessDetailsCountry" character varying(100)`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
15
+ );
16
+ }
17
17
 
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ await queryRunner.query(
20
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "Project" DROP COLUMN "businessDetailsCountry"`,
27
+ );
28
+ }
18
29
  }
@@ -0,0 +1,17 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1756300358095 implements MigrationInterface {
4
+ public name = "MigrationName1756300358095";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Project" ADD "financeAccountingEmail" character varying(100)`,
9
+ );
10
+ }
11
+
12
+ public async down(queryRunner: QueryRunner): Promise<void> {
13
+ await queryRunner.query(
14
+ `ALTER TABLE "Project" DROP COLUMN "financeAccountingEmail"`,
15
+ );
16
+ }
17
+ }
@@ -163,6 +163,7 @@ import { MigrationName1755778495455 } from "./1755778495455-MigrationName";
163
163
  import { MigrationName1755778934927 } from "./1755778934927-MigrationName";
164
164
  import { MigrationName1756293325324 } from "./1756293325324-MigrationName";
165
165
  import { MigrationName1756296282627 } from "./1756296282627-MigrationName";
166
+ import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
166
167
 
167
168
  export default [
168
169
  InitialMigration,
@@ -329,5 +330,6 @@ export default [
329
330
  MigrationName1755778495455,
330
331
  MigrationName1755778934927,
331
332
  MigrationName1756293325324,
332
- MigrationName1756296282627
333
+ MigrationName1756296282627,
334
+ MigrationName1756300358095,
333
335
  ];
@@ -85,6 +85,7 @@ export class BillingService extends BaseService {
85
85
  id: string,
86
86
  businessDetails: string,
87
87
  countryCode?: string | null,
88
+ financeAccountingEmail?: string | null,
88
89
  ): Promise<void> {
89
90
  if (!this.isBillingEnabled()) {
90
91
  throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
@@ -99,8 +100,12 @@ export class BillingService extends BaseService {
99
100
 
100
101
  const lines: Array<string> = businessDetails
101
102
  .split(/\r?\n/)
102
- .map((l: string) => l.trim())
103
- .filter((l: string) => l.length > 0);
103
+ .map((l: string) => {
104
+ return l.trim();
105
+ })
106
+ .filter((l: string) => {
107
+ return l.length > 0;
108
+ });
104
109
 
105
110
  let line1: string | undefined = undefined;
106
111
  let line2: string | undefined = undefined;
@@ -110,17 +115,34 @@ export class BillingService extends BaseService {
110
115
  line1 = first.substring(0, 200); // Stripe typical limit safeguard.
111
116
  }
112
117
  if (lines && lines.length > 1) {
113
- const rest: string = lines.slice(1).join(', ');
118
+ const rest: string = lines.slice(1).join(", ");
114
119
  line2 = rest.substring(0, 200);
115
120
  }
116
121
 
122
+ const metadata: Record<string, string> = {
123
+ business_details_full: businessDetails.substring(0, 5000),
124
+ };
125
+ if (financeAccountingEmail) {
126
+ metadata["finance_accounting_email"] = financeAccountingEmail.substring(
127
+ 0,
128
+ 200,
129
+ );
130
+ } else {
131
+ // Remove if cleared
132
+ metadata["finance_accounting_email"] = "";
133
+ }
134
+
117
135
  const updateParams: Stripe.CustomerUpdateParams = {
118
- metadata: {
119
- business_details_full: businessDetails.substring(0, 5000),
120
- },
136
+ metadata,
121
137
  address: {},
122
138
  };
123
139
 
140
+ // If finance / accounting email provided, set it as the customer email so Stripe sends
141
+ // invoices / receipts there. (Stripe only supports a single email via API currently.)
142
+ if (financeAccountingEmail && financeAccountingEmail.trim().length > 0) {
143
+ updateParams.email = financeAccountingEmail.trim();
144
+ }
145
+
124
146
  if (line1) {
125
147
  updateParams.address = updateParams.address || {};
126
148
  updateParams.address.line1 = line1;
@@ -138,8 +160,8 @@ export class BillingService extends BaseService {
138
160
  if (!line1 && !line2 && !countryCode) {
139
161
  // Clear address if empty details submitted.
140
162
  updateParams.address = {
141
- line1: '',
142
- line2: '',
163
+ line1: "",
164
+ line2: "",
143
165
  } as any;
144
166
  }
145
167
 
@@ -273,13 +273,17 @@ export class ProjectService extends DatabaseService<Model> {
273
273
  updateBy: UpdateBy<Model>,
274
274
  ): Promise<OnUpdate<Model>> {
275
275
  if (IsBillingEnabled) {
276
-
277
- if (updateBy.data.businessDetails || updateBy.data.businessDetailsCountry) {
276
+ if (
277
+ updateBy.data.businessDetails ||
278
+ updateBy.data.businessDetailsCountry ||
279
+ updateBy.data.financeAccountingEmail
280
+ ) {
278
281
  // Sync to Stripe.
279
282
  const project: Model | null = await this.findOneById({
280
283
  id: new ObjectID(updateBy.query._id! as string),
281
284
  select: {
282
285
  paymentProviderCustomerId: true,
286
+ financeAccountingEmail: true,
283
287
  },
284
288
  props: { isRoot: true },
285
289
  });
@@ -288,12 +292,15 @@ export class ProjectService extends DatabaseService<Model> {
288
292
  try {
289
293
  await BillingService.updateCustomerBusinessDetails(
290
294
  project.paymentProviderCustomerId,
291
- (updateBy.data.businessDetails as string) || '',
295
+ (updateBy.data.businessDetails as string) || "",
292
296
  (updateBy.data.businessDetailsCountry as string) || null,
297
+ (updateBy.data.financeAccountingEmail as string) ||
298
+ (project as any).financeAccountingEmail ||
299
+ null,
293
300
  );
294
301
  } catch (err) {
295
302
  logger.error(
296
- 'Failed to update Stripe customer business details: ' + err,
303
+ "Failed to update Stripe customer business details: " + err,
297
304
  );
298
305
  }
299
306
  }
@@ -340,10 +340,26 @@ export class Service extends DatabaseService<StatusPage> {
340
340
  projectId: ObjectID,
341
341
  statusPageId: ObjectID,
342
342
  ): Promise<URL> {
343
- const dahboardUrl: URL = await DatabaseConfig.getDashboardUrl();
343
+ if (!projectId) {
344
+ throw new BadDataException(
345
+ "projectId is required to build status page dashboard link",
346
+ );
347
+ }
348
+
349
+ if (!statusPageId) {
350
+ throw new BadDataException(
351
+ "statusPageId is required to build status page dashboard link",
352
+ );
353
+ }
354
+
355
+ const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
356
+
357
+ // Defensive: ensure objects have toString
358
+ const projectIdStr: string = projectId.toString();
359
+ const statusPageIdStr: string = statusPageId.toString();
344
360
 
345
- return URL.fromString(dahboardUrl.toString()).addRoute(
346
- `/${projectId.toString()}/status-pages/${statusPageId.toString()}`,
361
+ return URL.fromString(dashboardUrl.toString()).addRoute(
362
+ `/${projectIdStr}/status-pages/${statusPageIdStr}`,
347
363
  );
348
364
  }
349
365
 
@@ -203,30 +203,30 @@ export default class Markdown {
203
203
  return text as string;
204
204
  }
205
205
 
206
- const isHash: boolean = href.startsWith('#');
207
- const isMailTo: boolean = href.startsWith('mailto:');
208
- const isTel: boolean = href.startsWith('tel:');
206
+ const isHash: boolean = href.startsWith("#");
207
+ const isMailTo: boolean = href.startsWith("mailto:");
208
+ const isTel: boolean = href.startsWith("tel:");
209
209
  const isInternal: boolean =
210
- href.startsWith('/') ||
211
- href.includes('oneuptime.com') ||
210
+ href.startsWith("/") ||
211
+ href.includes("oneuptime.com") ||
212
212
  isHash ||
213
213
  isMailTo ||
214
214
  isTel;
215
215
 
216
216
  const baseClasses: string = [
217
- 'font-semibold',
218
- 'text-indigo-600',
219
- 'underline',
220
- 'underline-offset-2',
221
- 'decoration-indigo-300',
222
- 'hover:decoration-indigo-500',
223
- 'hover:text-indigo-500',
224
- 'transition-colors',
225
- ].join(' ');
226
-
227
- const titleAttr: string = title ? ` title="${title}"` : '';
217
+ "font-semibold",
218
+ "text-indigo-600",
219
+ "underline",
220
+ "underline-offset-2",
221
+ "decoration-indigo-300",
222
+ "hover:decoration-indigo-500",
223
+ "hover:text-indigo-500",
224
+ "transition-colors",
225
+ ].join(" ");
226
+
227
+ const titleAttr: string = title ? ` title="${title}"` : "";
228
228
  const externalAttrs: string = isInternal
229
- ? ''
229
+ ? ""
230
230
  : ' target="_blank" rel="noopener noreferrer"';
231
231
 
232
232
  return `<a href="${href}"${titleAttr} class="${baseClasses}"${externalAttrs}>${text}</a>`;
@@ -420,9 +420,8 @@ const Detail: DetailFunction = <T extends GenericObject>(
420
420
  )}
421
421
  </div>
422
422
  )}
423
- {(data === null || data === undefined || data === '') && field.placeholder && (
424
- <PlaceholderText text={field.placeholder} />
425
- )}
423
+ {(data === null || data === undefined || data === "") &&
424
+ field.placeholder && <PlaceholderText text={field.placeholder} />}
426
425
  </div>
427
426
  </div>
428
427
  );