@oneuptime/common 10.0.69 → 10.0.71
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/Models/DatabaseModels/KubernetesCluster.ts +7 -0
- package/Models/DatabaseModels/Project.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AIBillingService.ts +2 -2
- package/Server/Services/BillingService.ts +116 -48
- package/Server/Services/DatabaseService.ts +10 -27
- package/Server/Services/KubernetesResourceService.ts +33 -10
- package/Server/Services/NotificationService.ts +2 -2
- package/Server/Types/Database/QueryHelper.ts +127 -0
- package/Server/Types/Database/QueryUtil.ts +250 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +79 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +79 -0
- package/Types/BaseDatabase/EndsWith.ts +41 -0
- package/Types/BaseDatabase/IncludesAll.ts +45 -0
- package/Types/BaseDatabase/IncludesNone.ts +45 -0
- package/Types/BaseDatabase/NotContains.ts +41 -0
- package/Types/BaseDatabase/StartsWith.ts +41 -0
- package/Types/Email.ts +50 -0
- package/Types/JSON.ts +20 -0
- package/Types/SerializableObjectDictionary.ts +10 -0
- package/UI/Components/Filters/BooleanFilter.tsx +1 -0
- package/UI/Components/Filters/DateFilter.tsx +220 -25
- package/UI/Components/Filters/DropdownFilter.tsx +1 -0
- package/UI/Components/Filters/EntityFilter.tsx +229 -41
- package/UI/Components/Filters/FilterViewer.tsx +231 -147
- package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
- package/UI/Components/Filters/FiltersForm.tsx +146 -97
- package/UI/Components/Filters/NumberFilter.tsx +220 -34
- package/UI/Components/Filters/OperatorSelector.tsx +91 -0
- package/UI/Components/Filters/TextFilter.tsx +183 -71
- package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +10 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +9 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +5 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +125 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIBillingService.js +2 -2
- package/build/dist/Server/Services/AIBillingService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +99 -39
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +9 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +4 -2
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/NotificationService.js +2 -2
- package/build/dist/Server/Services/NotificationService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +186 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
- package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
- package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
- package/build/dist/Types/Email.js +42 -0
- package/build/dist/Types/Email.js.map +1 -1
- package/build/dist/Types/JSON.js +5 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +10 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DateFilter.js +155 -14
- package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +181 -30
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +188 -98
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
- package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js +164 -23
- package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
- package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
- package/build/dist/UI/Components/Filters/TextFilter.js +131 -53
- package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/package.json +1 -1
|
@@ -73,6 +73,13 @@ import {
|
|
|
73
73
|
})
|
|
74
74
|
@CrudApiEndpoint(new Route("/kubernetes-cluster"))
|
|
75
75
|
@SlugifyColumn("name", "slug")
|
|
76
|
+
/*
|
|
77
|
+
* Enforce one cluster row per (projectId, clusterIdentifier) at the DB level.
|
|
78
|
+
* Without this, two pods emitting OTel telemetry for a new cluster at the
|
|
79
|
+
* same time (e.g. when the agent is first installed or during a rolling
|
|
80
|
+
* update) race in findOrCreateByClusterIdentifier and create duplicate rows.
|
|
81
|
+
*/
|
|
82
|
+
@Index(["projectId", "clusterIdentifier"], { unique: true })
|
|
76
83
|
@TableMetadata({
|
|
77
84
|
tableName: "KubernetesCluster",
|
|
78
85
|
singularName: "Kubernetes Cluster",
|
|
@@ -342,15 +342,15 @@ export default class Project extends TenantModel {
|
|
|
342
342
|
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
343
343
|
})
|
|
344
344
|
@TableColumn({
|
|
345
|
-
type: TableColumnType.
|
|
345
|
+
type: TableColumnType.LongText,
|
|
346
346
|
title: "Finance / Accounting Email",
|
|
347
347
|
description:
|
|
348
|
-
"Invoices, receipts and billing related notifications will be sent to
|
|
349
|
-
example: "accounting@example.com",
|
|
348
|
+
"Invoices, receipts and billing related notifications will be sent to these emails in addition to project owner. Separate multiple emails with a comma.",
|
|
349
|
+
example: "accounting@example.com, finance@example.com",
|
|
350
350
|
})
|
|
351
351
|
@Column({
|
|
352
|
-
type: ColumnType.
|
|
353
|
-
length: ColumnLength.
|
|
352
|
+
type: ColumnType.LongText,
|
|
353
|
+
length: ColumnLength.LongText,
|
|
354
354
|
nullable: true,
|
|
355
355
|
unique: false,
|
|
356
356
|
})
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
2
|
|
|
3
3
|
export class MigrationName1776865086264 implements MigrationInterface {
|
|
4
|
-
|
|
4
|
+
public name: string = "MigrationName1776865086264";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await queryRunner.query(`ALTER TABLE "KubernetesResource" DROP COLUMN "containerCount"`);
|
|
12
|
-
}
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "KubernetesResource" ADD "containerCount" integer`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
13
11
|
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "KubernetesResource" DROP COLUMN "containerCount"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
14
17
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Before this migration, KubernetesCluster had an app-level
|
|
5
|
+
* @UniqueColumnBy("projectId") check and a non-unique index on
|
|
6
|
+
* clusterIdentifier, but no DB-level uniqueness. Under concurrent telemetry
|
|
7
|
+
* from multiple agent pods (happens every time the agent is installed or
|
|
8
|
+
* rolls out), findOrCreateByClusterIdentifier would race between its find
|
|
9
|
+
* and its create, and the DB accepted both inserts — producing duplicate
|
|
10
|
+
* rows with identical (projectId, clusterIdentifier).
|
|
11
|
+
*
|
|
12
|
+
* This migration:
|
|
13
|
+
* 1. Reparents all FKs that reference duplicate clusters — KubernetesResource,
|
|
14
|
+
* KubernetesClusterOwnerUser, KubernetesClusterOwnerTeam — onto the
|
|
15
|
+
* oldest surviving row in each duplicate group.
|
|
16
|
+
* 2. Deletes the duplicate (non-survivor) rows.
|
|
17
|
+
* 3. Creates a DB-level unique index on (projectId, clusterIdentifier) so
|
|
18
|
+
* future races are rejected by the DB — the service's existing
|
|
19
|
+
* catch-and-refetch in findOrCreateByClusterIdentifier then returns the
|
|
20
|
+
* winning row instead of producing a duplicate.
|
|
21
|
+
*
|
|
22
|
+
* The auto-generator also picked up unrelated OnCallDutyPolicyScheduleLayer
|
|
23
|
+
* default-value drift. That's dev-environment drift, not the bug we're fixing;
|
|
24
|
+
* stripped from this migration.
|
|
25
|
+
*/
|
|
26
|
+
export class DedupeKubernetesClustersAndAddUniqueIndex1776881254913
|
|
27
|
+
implements MigrationInterface
|
|
28
|
+
{
|
|
29
|
+
public name: string =
|
|
30
|
+
"DedupeKubernetesClustersAndAddUniqueIndex1776881254913";
|
|
31
|
+
|
|
32
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
33
|
+
// 1: reparent KubernetesResource FKs from duplicates -> survivor.
|
|
34
|
+
await queryRunner.query(`
|
|
35
|
+
WITH survivors AS (
|
|
36
|
+
SELECT DISTINCT ON ("projectId", "clusterIdentifier")
|
|
37
|
+
_id AS survivor_id,
|
|
38
|
+
"projectId",
|
|
39
|
+
"clusterIdentifier"
|
|
40
|
+
FROM "KubernetesCluster"
|
|
41
|
+
ORDER BY "projectId", "clusterIdentifier", "createdAt" ASC, _id ASC
|
|
42
|
+
),
|
|
43
|
+
losers AS (
|
|
44
|
+
SELECT kc._id AS loser_id, s.survivor_id
|
|
45
|
+
FROM "KubernetesCluster" kc
|
|
46
|
+
JOIN survivors s
|
|
47
|
+
ON s."projectId" = kc."projectId"
|
|
48
|
+
AND s."clusterIdentifier" = kc."clusterIdentifier"
|
|
49
|
+
WHERE kc._id <> s.survivor_id
|
|
50
|
+
)
|
|
51
|
+
UPDATE "KubernetesResource" kr
|
|
52
|
+
SET "kubernetesClusterId" = l.survivor_id
|
|
53
|
+
FROM losers l
|
|
54
|
+
WHERE kr."kubernetesClusterId" = l.loser_id;
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
// 2: reparent KubernetesClusterOwnerUser FKs.
|
|
58
|
+
await queryRunner.query(`
|
|
59
|
+
WITH survivors AS (
|
|
60
|
+
SELECT DISTINCT ON ("projectId", "clusterIdentifier")
|
|
61
|
+
_id AS survivor_id,
|
|
62
|
+
"projectId",
|
|
63
|
+
"clusterIdentifier"
|
|
64
|
+
FROM "KubernetesCluster"
|
|
65
|
+
ORDER BY "projectId", "clusterIdentifier", "createdAt" ASC, _id ASC
|
|
66
|
+
),
|
|
67
|
+
losers AS (
|
|
68
|
+
SELECT kc._id AS loser_id, s.survivor_id
|
|
69
|
+
FROM "KubernetesCluster" kc
|
|
70
|
+
JOIN survivors s
|
|
71
|
+
ON s."projectId" = kc."projectId"
|
|
72
|
+
AND s."clusterIdentifier" = kc."clusterIdentifier"
|
|
73
|
+
WHERE kc._id <> s.survivor_id
|
|
74
|
+
)
|
|
75
|
+
UPDATE "KubernetesClusterOwnerUser" o
|
|
76
|
+
SET "kubernetesClusterId" = l.survivor_id
|
|
77
|
+
FROM losers l
|
|
78
|
+
WHERE o."kubernetesClusterId" = l.loser_id;
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
// 3: reparent KubernetesClusterOwnerTeam FKs.
|
|
82
|
+
await queryRunner.query(`
|
|
83
|
+
WITH survivors AS (
|
|
84
|
+
SELECT DISTINCT ON ("projectId", "clusterIdentifier")
|
|
85
|
+
_id AS survivor_id,
|
|
86
|
+
"projectId",
|
|
87
|
+
"clusterIdentifier"
|
|
88
|
+
FROM "KubernetesCluster"
|
|
89
|
+
ORDER BY "projectId", "clusterIdentifier", "createdAt" ASC, _id ASC
|
|
90
|
+
),
|
|
91
|
+
losers AS (
|
|
92
|
+
SELECT kc._id AS loser_id, s.survivor_id
|
|
93
|
+
FROM "KubernetesCluster" kc
|
|
94
|
+
JOIN survivors s
|
|
95
|
+
ON s."projectId" = kc."projectId"
|
|
96
|
+
AND s."clusterIdentifier" = kc."clusterIdentifier"
|
|
97
|
+
WHERE kc._id <> s.survivor_id
|
|
98
|
+
)
|
|
99
|
+
UPDATE "KubernetesClusterOwnerTeam" o
|
|
100
|
+
SET "kubernetesClusterId" = l.survivor_id
|
|
101
|
+
FROM losers l
|
|
102
|
+
WHERE o."kubernetesClusterId" = l.loser_id;
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
// 4: delete duplicate rows now that nothing references them.
|
|
106
|
+
await queryRunner.query(`
|
|
107
|
+
WITH survivors AS (
|
|
108
|
+
SELECT DISTINCT ON ("projectId", "clusterIdentifier")
|
|
109
|
+
_id AS survivor_id,
|
|
110
|
+
"projectId",
|
|
111
|
+
"clusterIdentifier"
|
|
112
|
+
FROM "KubernetesCluster"
|
|
113
|
+
ORDER BY "projectId", "clusterIdentifier", "createdAt" ASC, _id ASC
|
|
114
|
+
)
|
|
115
|
+
DELETE FROM "KubernetesCluster" kc
|
|
116
|
+
USING survivors s
|
|
117
|
+
WHERE s."projectId" = kc."projectId"
|
|
118
|
+
AND s."clusterIdentifier" = kc."clusterIdentifier"
|
|
119
|
+
AND kc._id <> s.survivor_id;
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
// 5: add the DB-level composite unique index.
|
|
123
|
+
await queryRunner.query(
|
|
124
|
+
`CREATE UNIQUE INDEX "IDX_9756988b48848f4f7532a2af0d" ON "KubernetesCluster" ("projectId", "clusterIdentifier") `,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
129
|
+
await queryRunner.query(
|
|
130
|
+
`DROP INDEX "public"."IDX_9756988b48848f4f7532a2af0d"`,
|
|
131
|
+
);
|
|
132
|
+
/*
|
|
133
|
+
* Duplicate rows dropped in up() are lost — a down-migration cannot
|
|
134
|
+
* resurrect them (and reinstating duplicates is not desirable anyway).
|
|
135
|
+
*/
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1776886248361 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1776886248361";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Project" ALTER COLUMN "financeAccountingEmail" TYPE character varying(500)`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "Project" ALTER COLUMN "financeAccountingEmail" TYPE character varying(100)`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -288,6 +288,7 @@ import { MigrationName1776544084793 } from "./1776544084793-MigrationName";
|
|
|
288
288
|
import { MigrationName1776761171349 } from "./1776761171349-MigrationName";
|
|
289
289
|
import { MigrationName1776801030808 } from "./1776801030808-MigrationName";
|
|
290
290
|
import { MigrationName1776865086264 } from "./1776865086264-MigrationName";
|
|
291
|
+
import { DedupeKubernetesClustersAndAddUniqueIndex1776881254913 } from "./1776881254913-DedupeKubernetesClustersAndAddUniqueIndex";
|
|
291
292
|
export default [
|
|
292
293
|
InitialMigration,
|
|
293
294
|
MigrationName1717678334852,
|
|
@@ -579,4 +580,5 @@ export default [
|
|
|
579
580
|
MigrationName1776761171349,
|
|
580
581
|
MigrationName1776801030808,
|
|
581
582
|
MigrationName1776865086264,
|
|
583
|
+
DedupeKubernetesClustersAndAddUniqueIndex1776881254913,
|
|
582
584
|
];
|
|
@@ -94,8 +94,8 @@ export class AIBillingService extends BaseService {
|
|
|
94
94
|
amountInUSD,
|
|
95
95
|
{
|
|
96
96
|
sendInvoiceByEmail: project.sendInvoicesByEmail || false,
|
|
97
|
-
|
|
98
|
-
?
|
|
97
|
+
recipientEmails: project.financeAccountingEmail
|
|
98
|
+
? Email.parseList(project.financeAccountingEmail)
|
|
99
99
|
: undefined,
|
|
100
100
|
projectId: project.id || undefined,
|
|
101
101
|
},
|
|
@@ -138,13 +138,37 @@ export class BillingService extends BaseService {
|
|
|
138
138
|
line2 = rest.substring(0, 200);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/*
|
|
142
|
+
* financeAccountingEmail may be a comma/semicolon-separated list. Parse the first
|
|
143
|
+
* valid address for Stripe (which stores a single email), and keep the full list
|
|
144
|
+
* in metadata so OneUptime can fan out invoice emails to every recipient.
|
|
145
|
+
*/
|
|
146
|
+
const parsedEmails: Array<Email> = financeAccountingEmail
|
|
147
|
+
? (() => {
|
|
148
|
+
try {
|
|
149
|
+
return Email.parseList(financeAccountingEmail);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error(
|
|
152
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for customer ${id}: ${err}`,
|
|
153
|
+
);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const normalizedEmailList: string = parsedEmails
|
|
160
|
+
.map((e: Email): string => {
|
|
161
|
+
return e.toString();
|
|
162
|
+
})
|
|
163
|
+
.join(", ");
|
|
164
|
+
|
|
141
165
|
const metadata: Record<string, string> = {
|
|
142
166
|
business_details_full: businessDetails.substring(0, 5000),
|
|
143
167
|
};
|
|
144
|
-
if (
|
|
145
|
-
metadata["finance_accounting_email"] =
|
|
168
|
+
if (normalizedEmailList) {
|
|
169
|
+
metadata["finance_accounting_email"] = normalizedEmailList.substring(
|
|
146
170
|
0,
|
|
147
|
-
|
|
171
|
+
500,
|
|
148
172
|
);
|
|
149
173
|
} else {
|
|
150
174
|
// Remove if cleared
|
|
@@ -165,11 +189,11 @@ export class BillingService extends BaseService {
|
|
|
165
189
|
};
|
|
166
190
|
|
|
167
191
|
/*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
192
|
+
* Stripe's customer.email only accepts one address. Use the first parsed email;
|
|
193
|
+
* the rest are delivered by OneUptime's own webhook-driven invoice email path.
|
|
170
194
|
*/
|
|
171
|
-
if (
|
|
172
|
-
updateParams.email =
|
|
195
|
+
if (parsedEmails.length > 0) {
|
|
196
|
+
updateParams.email = parsedEmails[0]!.toString();
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
if (line1) {
|
|
@@ -956,11 +980,16 @@ export class BillingService extends BaseService {
|
|
|
956
980
|
@CaptureSpan()
|
|
957
981
|
public async sendInvoiceByEmail(
|
|
958
982
|
invoiceId: string,
|
|
959
|
-
|
|
983
|
+
recipientEmails?: Array<Email>,
|
|
960
984
|
projectId?: ObjectID,
|
|
961
985
|
): Promise<void> {
|
|
986
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
987
|
+
.map((e: Email): string => {
|
|
988
|
+
return e.toString();
|
|
989
|
+
})
|
|
990
|
+
.join(", ");
|
|
962
991
|
logger.debug(
|
|
963
|
-
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId},
|
|
992
|
+
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId}, recipients: ${recipientsForLog}`,
|
|
964
993
|
);
|
|
965
994
|
|
|
966
995
|
if (!this.isBillingEnabled()) {
|
|
@@ -986,13 +1015,22 @@ export class BillingService extends BaseService {
|
|
|
986
1015
|
return;
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1018
|
+
/*
|
|
1019
|
+
* Determine recipient list. Fall back to the invoice's customer email when no
|
|
1020
|
+
* explicit recipients were supplied by the caller.
|
|
1021
|
+
*/
|
|
1022
|
+
let toEmails: Array<Email> = recipientEmails || [];
|
|
1023
|
+
if (toEmails.length === 0 && stripeInvoice.customer_email) {
|
|
1024
|
+
try {
|
|
1025
|
+
toEmails = Email.parseList(stripeInvoice.customer_email);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
logger.error(
|
|
1028
|
+
`[Invoice Email] Failed to parse Stripe customer_email "${stripeInvoice.customer_email}" for invoice ${invoiceId}: ${err}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
993
1031
|
}
|
|
994
1032
|
|
|
995
|
-
if (
|
|
1033
|
+
if (toEmails.length === 0) {
|
|
996
1034
|
logger.error(
|
|
997
1035
|
`[Invoice Email] No recipient email found for invoice ${invoiceId}`,
|
|
998
1036
|
{ projectId: projectId?.toString() } as LogAttributes,
|
|
@@ -1021,33 +1059,41 @@ export class BillingService extends BaseService {
|
|
|
1021
1059
|
dashboardLink = `${DashboardClientUrl.toString()}/dashboard/${projectId.toString()}/settings/billing`;
|
|
1022
1060
|
}
|
|
1023
1061
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1062
|
+
for (const toEmail of toEmails) {
|
|
1063
|
+
logger.debug(
|
|
1064
|
+
`[Invoice Email] Sending invoice email to ${toEmail.toString()} - Invoice #${invoiceNumber}, Amount: ${amount}`,
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
await MailService.sendMail(
|
|
1068
|
+
{
|
|
1069
|
+
toEmail: toEmail,
|
|
1070
|
+
templateType: EmailTemplateType.Invoice,
|
|
1071
|
+
vars: {
|
|
1072
|
+
invoiceNumber: invoiceNumber,
|
|
1073
|
+
invoiceDate: invoiceDate,
|
|
1074
|
+
amount: amount,
|
|
1075
|
+
description: description || "",
|
|
1076
|
+
invoicePdfUrl: invoicePdfUrl || "",
|
|
1077
|
+
dashboardLink: dashboardLink || "",
|
|
1078
|
+
},
|
|
1079
|
+
subject: `Invoice #${invoiceNumber} from OneUptime`,
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
projectId: projectId,
|
|
1083
|
+
},
|
|
1084
|
+
);
|
|
1047
1085
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1086
|
+
logger.debug(
|
|
1087
|
+
`[Invoice Email] Successfully sent invoice ${invoiceId} email to ${toEmail.toString()}`,
|
|
1088
|
+
);
|
|
1089
|
+
} catch (perRecipientErr) {
|
|
1090
|
+
// Keep going for the remaining recipients if one fails.
|
|
1091
|
+
logger.error(
|
|
1092
|
+
`[Invoice Email] Failed to send invoice ${invoiceId} to ${toEmail.toString()}: ${perRecipientErr}`,
|
|
1093
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1051
1097
|
} catch (err) {
|
|
1052
1098
|
logger.error(
|
|
1053
1099
|
`[Invoice Email] Failed to send invoice ${invoiceId} by email: ${err}`,
|
|
@@ -1108,16 +1154,21 @@ export class BillingService extends BaseService {
|
|
|
1108
1154
|
amountInUsd: number,
|
|
1109
1155
|
options?: {
|
|
1110
1156
|
sendInvoiceByEmail?: boolean | undefined;
|
|
1111
|
-
|
|
1157
|
+
recipientEmails?: Array<Email> | undefined;
|
|
1112
1158
|
projectId?: ObjectID | undefined;
|
|
1113
1159
|
},
|
|
1114
1160
|
): Promise<void> {
|
|
1115
1161
|
const sendInvoiceByEmail: boolean = options?.sendInvoiceByEmail || false;
|
|
1116
|
-
const
|
|
1162
|
+
const recipientEmails: Array<Email> | undefined = options?.recipientEmails;
|
|
1117
1163
|
const projectId: ObjectID | undefined = options?.projectId;
|
|
1118
1164
|
|
|
1165
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
1166
|
+
.map((e: Email): string => {
|
|
1167
|
+
return e.toString();
|
|
1168
|
+
})
|
|
1169
|
+
.join(", ");
|
|
1119
1170
|
logger.debug(
|
|
1120
|
-
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail},
|
|
1171
|
+
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail}, recipients: ${recipientsForLog}, projectId: ${projectId?.toString()}`,
|
|
1121
1172
|
);
|
|
1122
1173
|
|
|
1123
1174
|
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
|
|
@@ -1161,7 +1212,7 @@ export class BillingService extends BaseService {
|
|
|
1161
1212
|
logger.debug(
|
|
1162
1213
|
`[Invoice Email] sendInvoiceByEmail is true, sending invoice ${invoice.id} by email`,
|
|
1163
1214
|
);
|
|
1164
|
-
await this.sendInvoiceByEmail(invoice.id!,
|
|
1215
|
+
await this.sendInvoiceByEmail(invoice.id!, recipientEmails, projectId);
|
|
1165
1216
|
} else {
|
|
1166
1217
|
logger.debug(
|
|
1167
1218
|
`[Invoice Email] sendInvoiceByEmail is false, skipping email for invoice ${invoice.id}`,
|
|
@@ -1356,16 +1407,33 @@ export class BillingService extends BaseService {
|
|
|
1356
1407
|
},
|
|
1357
1408
|
});
|
|
1358
1409
|
|
|
1359
|
-
let
|
|
1410
|
+
let recipientEmails: Array<Email> | undefined = undefined;
|
|
1360
1411
|
let projectId: ObjectID | undefined = undefined;
|
|
1361
1412
|
|
|
1362
1413
|
if (project) {
|
|
1363
1414
|
projectId = project.id || undefined;
|
|
1364
1415
|
if (project.financeAccountingEmail) {
|
|
1365
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
recipientEmails = Email.parseList(
|
|
1418
|
+
project.financeAccountingEmail,
|
|
1419
|
+
);
|
|
1420
|
+
} catch (parseErr) {
|
|
1421
|
+
/*
|
|
1422
|
+
* Log and fall back to Stripe customer email rather than dropping
|
|
1423
|
+
* the invoice delivery entirely due to a malformed address.
|
|
1424
|
+
*/
|
|
1425
|
+
logger.error(
|
|
1426
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for project ${projectId?.toString()}: ${parseErr}`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1366
1429
|
}
|
|
1430
|
+
const parsedForLog: string = (recipientEmails || [])
|
|
1431
|
+
.map((e: Email): string => {
|
|
1432
|
+
return e.toString();
|
|
1433
|
+
})
|
|
1434
|
+
.join(", ");
|
|
1367
1435
|
logger.debug(
|
|
1368
|
-
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail: ${
|
|
1436
|
+
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail recipients: ${parsedForLog}`,
|
|
1369
1437
|
);
|
|
1370
1438
|
} else {
|
|
1371
1439
|
logger.debug(
|
|
@@ -1376,7 +1444,7 @@ export class BillingService extends BaseService {
|
|
|
1376
1444
|
logger.debug(
|
|
1377
1445
|
`[Invoice Email] Sending invoice ${invoice.id} by email`,
|
|
1378
1446
|
);
|
|
1379
|
-
await this.sendInvoiceByEmail(invoice.id,
|
|
1447
|
+
await this.sendInvoiceByEmail(invoice.id, recipientEmails, projectId);
|
|
1380
1448
|
logger.debug(
|
|
1381
1449
|
`[Invoice Email] Successfully processed invoice.finalized - sent invoice ${invoice.id} by email`,
|
|
1382
1450
|
);
|
|
@@ -69,6 +69,7 @@ import { FindWhere } from "../../Types/BaseDatabase/Query";
|
|
|
69
69
|
import Realtime from "../Utils/Realtime";
|
|
70
70
|
import ModelEventType from "../../Types/Realtime/ModelEventType";
|
|
71
71
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
72
|
+
import type AuditLogServiceType from "./AuditLogService";
|
|
72
73
|
|
|
73
74
|
class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
74
75
|
public modelType!: { new (): TBaseModel };
|
|
@@ -782,14 +783,9 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
782
783
|
* which extend DatabaseService). A top-level import leaves
|
|
783
784
|
* DatabaseService undefined at class-extension time for subclasses.
|
|
784
785
|
*/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
model: TBaseModel;
|
|
789
|
-
createdItem: TBaseModel;
|
|
790
|
-
props: DatabaseCommonInteractionProps;
|
|
791
|
-
}) => Promise<void>;
|
|
792
|
-
} = require("./AuditLogService").default;
|
|
786
|
+
const auditLogService: typeof AuditLogServiceType =
|
|
787
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
788
|
+
require("./AuditLogService").default;
|
|
793
789
|
await auditLogService.recordCreate({
|
|
794
790
|
model: this.getModel(),
|
|
795
791
|
createdItem: createBy.data,
|
|
@@ -1237,15 +1233,9 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1237
1233
|
}
|
|
1238
1234
|
|
|
1239
1235
|
if (this.getModel().enableAuditLogOn?.delete && items.length > 0) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
model: TBaseModel;
|
|
1244
|
-
deletedItem: TBaseModel;
|
|
1245
|
-
itemId: ObjectID;
|
|
1246
|
-
props: DatabaseCommonInteractionProps;
|
|
1247
|
-
}) => Promise<void>;
|
|
1248
|
-
} = require("./AuditLogService").default;
|
|
1236
|
+
const auditLogService: typeof AuditLogServiceType =
|
|
1237
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
1238
|
+
require("./AuditLogService").default;
|
|
1249
1239
|
for (const item of items) {
|
|
1250
1240
|
if (item.id) {
|
|
1251
1241
|
await auditLogService.recordDelete({
|
|
@@ -1667,16 +1657,9 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1667
1657
|
!this.hasSameValues({ item, updatedItem }) &&
|
|
1668
1658
|
item.id
|
|
1669
1659
|
) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
model: TBaseModel;
|
|
1674
|
-
before: TBaseModel;
|
|
1675
|
-
updatedFields: JSONObject;
|
|
1676
|
-
itemId: ObjectID;
|
|
1677
|
-
props: DatabaseCommonInteractionProps;
|
|
1678
|
-
}) => Promise<void>;
|
|
1679
|
-
} = require("./AuditLogService").default;
|
|
1660
|
+
const auditLogService: typeof AuditLogServiceType =
|
|
1661
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
1662
|
+
require("./AuditLogService").default;
|
|
1680
1663
|
await auditLogService.recordUpdate({
|
|
1681
1664
|
model: this.getModel(),
|
|
1682
1665
|
before: item,
|
|
@@ -113,7 +113,10 @@ function buildDegradedPod(row: {
|
|
|
113
113
|
const scanForReason: (
|
|
114
114
|
list: Array<Record<string, unknown>>,
|
|
115
115
|
targetState: string,
|
|
116
|
-
) => { reason: string; message: string } | null = (
|
|
116
|
+
) => { reason: string; message: string } | null = (
|
|
117
|
+
list: Array<Record<string, unknown>>,
|
|
118
|
+
targetState: string,
|
|
119
|
+
) => {
|
|
117
120
|
for (const cs of list) {
|
|
118
121
|
if (cs["state"] !== targetState) {
|
|
119
122
|
continue;
|
|
@@ -144,8 +147,10 @@ function buildDegradedPod(row: {
|
|
|
144
147
|
reason = hit.reason;
|
|
145
148
|
message = hit.message;
|
|
146
149
|
} else {
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
/*
|
|
151
|
+
* Fall back to the pod-level reason/message fields set by the scheduler
|
|
152
|
+
* (e.g. "Unschedulable" with "0/3 nodes are available: ...").
|
|
153
|
+
*/
|
|
149
154
|
const topReason: unknown = status["reason"];
|
|
150
155
|
const topMessage: unknown = status["message"];
|
|
151
156
|
if (typeof topReason === "string") {
|
|
@@ -211,7 +216,9 @@ function buildDegradedNode(row: {
|
|
|
211
216
|
|
|
212
217
|
const findCondition: (
|
|
213
218
|
predicate: (c: Record<string, unknown>) => boolean,
|
|
214
|
-
) => Record<string, unknown> | null = (
|
|
219
|
+
) => Record<string, unknown> | null = (
|
|
220
|
+
predicate: (c: Record<string, unknown>) => boolean,
|
|
221
|
+
) => {
|
|
215
222
|
for (const c of conditions) {
|
|
216
223
|
if (predicate(c)) {
|
|
217
224
|
return c;
|
|
@@ -591,12 +598,28 @@ export class Service extends DatabaseService<Model> {
|
|
|
591
598
|
const containerCount: number =
|
|
592
599
|
parseInt(containerRows[0]?.total || "0", 10) || 0;
|
|
593
600
|
|
|
594
|
-
const degradedPods: Array<DegradedPod> = degradedPodRows.map(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
601
|
+
const degradedPods: Array<DegradedPod> = degradedPodRows.map(
|
|
602
|
+
(row: {
|
|
603
|
+
name: string;
|
|
604
|
+
namespaceKey: string;
|
|
605
|
+
phase: string | null;
|
|
606
|
+
status: unknown;
|
|
607
|
+
}) => {
|
|
608
|
+
return buildDegradedPod(row);
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
const degradedNodes: Array<DegradedNode> = degradedNodeRows.map(
|
|
612
|
+
(row: {
|
|
613
|
+
name: string;
|
|
614
|
+
isReady: boolean | null;
|
|
615
|
+
hasMemoryPressure: boolean | null;
|
|
616
|
+
hasDiskPressure: boolean | null;
|
|
617
|
+
hasPidPressure: boolean | null;
|
|
618
|
+
status: unknown;
|
|
619
|
+
}) => {
|
|
620
|
+
return buildDegradedNode(row);
|
|
621
|
+
},
|
|
622
|
+
);
|
|
600
623
|
|
|
601
624
|
return {
|
|
602
625
|
countsByKind,
|
|
@@ -90,8 +90,8 @@ export class NotificationService extends BaseService {
|
|
|
90
90
|
amountInUSD,
|
|
91
91
|
{
|
|
92
92
|
sendInvoiceByEmail: project.sendInvoicesByEmail || false,
|
|
93
|
-
|
|
94
|
-
?
|
|
93
|
+
recipientEmails: project.financeAccountingEmail
|
|
94
|
+
? Email.parseList(project.financeAccountingEmail)
|
|
95
95
|
: undefined,
|
|
96
96
|
projectId: project.id || undefined,
|
|
97
97
|
},
|