vidspotai-shared 1.0.105 → 1.0.107-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/globals/aiModels/enums.d.ts +0 -2
- package/lib/globals/aiModels/enums.d.ts.map +1 -1
- package/lib/globals/aiModels/enums.js +0 -2
- package/lib/globals/aiModels/providers/bytedance.d.ts.map +1 -1
- package/lib/globals/aiModels/providers/bytedance.js +4 -89
- package/lib/globals/types.d.ts +3 -1
- package/lib/globals/types.d.ts.map +1 -1
- package/lib/globals/types.js +8 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.d.ts.map +1 -1
- package/lib/models/index.js +1 -0
- package/lib/models/organization.model.d.ts +72 -0
- package/lib/models/organization.model.d.ts.map +1 -0
- package/lib/models/organization.model.js +6 -0
- package/lib/models/user.model.d.ts +12 -0
- package/lib/models/user.model.d.ts.map +1 -1
- package/lib/models/video.model.d.ts +7 -0
- package/lib/models/video.model.d.ts.map +1 -1
- package/lib/services/agent/providerFallback/chains.d.ts.map +1 -1
- package/lib/services/agent/providerFallback/chains.js +0 -8
- package/lib/services/agent/tools/generateVideo.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/generateVideo.tool.js +0 -1
- package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
- package/lib/services/aiGen/aiGenFactory.service.js +0 -2
- package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/bytedance/bytedance.service.js +3 -6
- package/lib/services/credit.service.d.ts +4 -2
- package/lib/services/credit.service.d.ts.map +1 -1
- package/lib/services/credit.service.js +113 -2
- package/lib/services/email/index.d.ts +3 -0
- package/lib/services/email/index.d.ts.map +1 -0
- package/lib/services/email/index.js +18 -0
- package/lib/services/email/orgInvite.d.ts +25 -0
- package/lib/services/email/orgInvite.d.ts.map +1 -0
- package/lib/services/email/orgInvite.js +113 -0
- package/lib/services/email/resendClient.d.ts +12 -0
- package/lib/services/email/resendClient.d.ts.map +1 -0
- package/lib/services/email/resendClient.js +39 -0
- package/lib/services/firestore.service.d.ts +5 -0
- package/lib/services/firestore.service.d.ts.map +1 -1
- package/lib/services/firestore.service.js +13 -0
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +2 -0
- package/lib/services/organizationCredit.service.d.ts +36 -0
- package/lib/services/organizationCredit.service.d.ts.map +1 -0
- package/lib/services/organizationCredit.service.js +105 -0
- package/lib/services/translation/translation.service.d.ts.map +1 -1
- package/lib/services/translation/translation.service.js +9 -23
- package/package.json +2 -1
|
@@ -5,6 +5,7 @@ const firestore_1 = require("firebase-admin/firestore");
|
|
|
5
5
|
const types_1 = require("../globals/types");
|
|
6
6
|
const firebase_1 = require("../libs/firebase");
|
|
7
7
|
const logger_1 = require("../utils/logger");
|
|
8
|
+
const organization_model_1 = require("../models/organization.model");
|
|
8
9
|
const firestore_service_1 = require("./firestore.service");
|
|
9
10
|
const notification_service_1 = require("./notification.service");
|
|
10
11
|
// Once balance drops below this fraction of total, fire CREDITS_LOW. The
|
|
@@ -25,6 +26,63 @@ class CreditService {
|
|
|
25
26
|
throw new Error("USER_NOT_FOUND");
|
|
26
27
|
}
|
|
27
28
|
const data = snap.data();
|
|
29
|
+
// Enterprise Organizations: a member spends from their org's pooled
|
|
30
|
+
// balance instead of their own — transparent to every call site (~30+
|
|
31
|
+
// across controllers/queue-workers/mcp-server all just call
|
|
32
|
+
// consumeCredits(userId, n)). Must live in THIS transaction — Firestore
|
|
33
|
+
// doesn't support nested transactions, so this can't call out to
|
|
34
|
+
// OrganizationCreditService, which owns the separate (non-nested)
|
|
35
|
+
// owner-tops-up-pool transaction.
|
|
36
|
+
const orgId = data.organizationId;
|
|
37
|
+
if (orgId) {
|
|
38
|
+
const orgRef = firestore_service_1.FirestoreService.organizationsCol.doc(orgId);
|
|
39
|
+
const memberRef = firestore_service_1.FirestoreService.organizationMembersCol.doc((0, organization_model_1.organizationMemberDocId)(orgId, userId));
|
|
40
|
+
const [orgSnap, memberSnap] = await Promise.all([
|
|
41
|
+
tx.get(orgRef),
|
|
42
|
+
tx.get(memberRef),
|
|
43
|
+
]);
|
|
44
|
+
if (!orgSnap.exists)
|
|
45
|
+
throw new Error("ORG_NOT_FOUND");
|
|
46
|
+
if (!memberSnap.exists)
|
|
47
|
+
throw new Error("ORG_MEMBER_NOT_FOUND");
|
|
48
|
+
const orgData = orgSnap.data();
|
|
49
|
+
const orgConsumed = orgData?.credits?.consumed ?? 0;
|
|
50
|
+
const orgTotal = orgData?.credits?.total ?? 0;
|
|
51
|
+
if (orgConsumed + creditsToConsume > orgTotal) {
|
|
52
|
+
throw new Error("ORG_INSUFFICIENT_CREDITS");
|
|
53
|
+
}
|
|
54
|
+
const allocation = memberSnap.data()?.monthlyAllocation;
|
|
55
|
+
if (orgData?.creditMode === "allocated" && allocation) {
|
|
56
|
+
const memberConsumed = allocation.consumed ?? 0;
|
|
57
|
+
if (memberConsumed + creditsToConsume > allocation.total) {
|
|
58
|
+
throw new Error("ORG_MEMBER_ALLOCATION_EXCEEDED");
|
|
59
|
+
}
|
|
60
|
+
tx.update(memberRef, {
|
|
61
|
+
"monthlyAllocation.consumed": memberConsumed + creditsToConsume,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const newOrgConsumed = orgConsumed + creditsToConsume;
|
|
65
|
+
tx.update(orgRef, { "credits.consumed": newOrgConsumed });
|
|
66
|
+
const ledgerRef = firestore_service_1.FirestoreService.organizationCreditLedgerCol.doc();
|
|
67
|
+
tx.set(ledgerRef, {
|
|
68
|
+
id: ledgerRef.id,
|
|
69
|
+
orgId,
|
|
70
|
+
actorUid: userId,
|
|
71
|
+
delta: -creditsToConsume,
|
|
72
|
+
reason: "consume",
|
|
73
|
+
balanceAfter: orgTotal - newOrgConsumed,
|
|
74
|
+
createdAt: firestore_1.FieldValue.serverTimestamp(),
|
|
75
|
+
});
|
|
76
|
+
// Signal the org path to the caller so the personal CREDITS_LOW flow
|
|
77
|
+
// below is skipped — org-pool-low notifications are a fast-follow.
|
|
78
|
+
return {
|
|
79
|
+
orgSpend: true,
|
|
80
|
+
chargedOrgId: orgId,
|
|
81
|
+
total: 0,
|
|
82
|
+
newRemaining: 0,
|
|
83
|
+
lastNotifiedMs: 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
28
86
|
const consumed = data.credits?.consumed ?? 0;
|
|
29
87
|
const total = data.credits?.total ?? 0;
|
|
30
88
|
if (consumed + creditsToConsume > total) {
|
|
@@ -35,8 +93,16 @@ class CreditService {
|
|
|
35
93
|
});
|
|
36
94
|
const newRemaining = total - (consumed + creditsToConsume);
|
|
37
95
|
const lastNotifiedMs = data.lastCreditsLowNotifiedAt?.toMillis?.() ?? 0;
|
|
38
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
orgSpend: false,
|
|
98
|
+
chargedOrgId: null,
|
|
99
|
+
total,
|
|
100
|
+
newRemaining,
|
|
101
|
+
lastNotifiedMs,
|
|
102
|
+
};
|
|
39
103
|
});
|
|
104
|
+
if (postState.orgSpend)
|
|
105
|
+
return { chargedOrgId: postState.chargedOrgId };
|
|
40
106
|
const { total, newRemaining, lastNotifiedMs } = postState;
|
|
41
107
|
if (total > 0 && newRemaining < total * CREDITS_LOW_THRESHOLD) {
|
|
42
108
|
const sinceLast = Date.now() - lastNotifiedMs;
|
|
@@ -62,8 +128,9 @@ class CreditService {
|
|
|
62
128
|
});
|
|
63
129
|
}
|
|
64
130
|
}
|
|
131
|
+
return { chargedOrgId: postState.chargedOrgId };
|
|
65
132
|
}
|
|
66
|
-
static async refundCredits(userId, creditsToRefund) {
|
|
133
|
+
static async refundCredits(userId, creditsToRefund, spentFromOrgId) {
|
|
67
134
|
if (creditsToRefund <= 0)
|
|
68
135
|
return;
|
|
69
136
|
await firebase_1.firestore.runTransaction(async (tx) => {
|
|
@@ -72,6 +139,50 @@ class CreditService {
|
|
|
72
139
|
if (!snap.exists) {
|
|
73
140
|
throw new Error("USER_NOT_FOUND");
|
|
74
141
|
}
|
|
142
|
+
// `spentFromOrgId` (from the job's `spentFrom` snapshot) takes priority
|
|
143
|
+
// over the user's *current* organizationId — the active org may have
|
|
144
|
+
// changed between the charge and this refund. Callers with no job-doc
|
|
145
|
+
// concept (older refund sites) omit it and fall back to today's
|
|
146
|
+
// re-derive-from-current-org behavior.
|
|
147
|
+
const orgId = spentFromOrgId ?? snap.data()?.organizationId;
|
|
148
|
+
if (orgId) {
|
|
149
|
+
const orgRef = firestore_service_1.FirestoreService.organizationsCol.doc(orgId);
|
|
150
|
+
const memberRef = firestore_service_1.FirestoreService.organizationMembersCol.doc((0, organization_model_1.organizationMemberDocId)(orgId, userId));
|
|
151
|
+
const [orgSnap, memberSnap] = await Promise.all([
|
|
152
|
+
tx.get(orgRef),
|
|
153
|
+
tx.get(memberRef),
|
|
154
|
+
]);
|
|
155
|
+
// Org/membership may have been deleted since the charge — swallow
|
|
156
|
+
// like the personal path's own "can't go below 0" clamp; there's
|
|
157
|
+
// nowhere left to refund to.
|
|
158
|
+
if (!orgSnap.exists)
|
|
159
|
+
return;
|
|
160
|
+
const orgData = orgSnap.data();
|
|
161
|
+
const orgConsumed = orgData?.credits?.consumed ?? 0;
|
|
162
|
+
const orgTotal = orgData?.credits?.total ?? 0;
|
|
163
|
+
const newOrgConsumed = Math.max(0, orgConsumed - creditsToRefund);
|
|
164
|
+
tx.update(orgRef, { "credits.consumed": newOrgConsumed });
|
|
165
|
+
const allocation = memberSnap.exists
|
|
166
|
+
? memberSnap.data()?.monthlyAllocation
|
|
167
|
+
: undefined;
|
|
168
|
+
if (orgData?.creditMode === "allocated" && allocation && memberSnap.exists) {
|
|
169
|
+
const newMemberConsumed = Math.max(0, (allocation.consumed ?? 0) - creditsToRefund);
|
|
170
|
+
tx.update(memberRef, {
|
|
171
|
+
"monthlyAllocation.consumed": newMemberConsumed,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const ledgerRef = firestore_service_1.FirestoreService.organizationCreditLedgerCol.doc();
|
|
175
|
+
tx.set(ledgerRef, {
|
|
176
|
+
id: ledgerRef.id,
|
|
177
|
+
orgId,
|
|
178
|
+
actorUid: userId,
|
|
179
|
+
delta: creditsToRefund,
|
|
180
|
+
reason: "refund",
|
|
181
|
+
balanceAfter: orgTotal - newOrgConsumed,
|
|
182
|
+
createdAt: firestore_1.FieldValue.serverTimestamp(),
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
75
186
|
const consumed = snap.data()?.credits?.consumed ?? 0;
|
|
76
187
|
// Never let consumed go below 0
|
|
77
188
|
const newConsumed = Math.max(0, consumed - creditsToRefund);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/email/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
__exportStar(require("./resendClient"), exports);
|
|
18
|
+
__exportStar(require("./orgInvite"), exports);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TOrgRole } from "../../models/organization.model";
|
|
2
|
+
export interface SendOrgInviteEmailInput {
|
|
3
|
+
/** Invitee's email address. */
|
|
4
|
+
to: string;
|
|
5
|
+
orgName: string;
|
|
6
|
+
/** Display name (or email, if no name set) of the org member sending the invite. */
|
|
7
|
+
inviterName: string;
|
|
8
|
+
role: TOrgRole;
|
|
9
|
+
/** Full accept-invite URL, e.g. `${FRONTEND_URL}/invite/${token}`. */
|
|
10
|
+
acceptUrl: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SendOrgInviteEmailResult {
|
|
13
|
+
/** Resend message id. */
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sends the org-invite email via Resend. Throws on misconfiguration or a
|
|
18
|
+
* Resend-reported failure — this is intentional: callers that want
|
|
19
|
+
* best-effort semantics (the invite doc + copyable link are already the
|
|
20
|
+
* source of truth) should call this WITHOUT awaiting and chain `.catch()` to
|
|
21
|
+
* log rather than fail the parent request. See
|
|
22
|
+
* `organization.controller.ts#createInvite`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sendOrgInviteEmail(input: SendOrgInviteEmailInput): Promise<SendOrgInviteEmailResult>;
|
|
25
|
+
//# sourceMappingURL=orgInvite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgInvite.d.ts","sourceRoot":"","sources":["../../../src/services/email/orgInvite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAM3D,MAAM,WAAW,uBAAuB;IACtC,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;CACZ;AAiFD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,wBAAwB,CAAC,CA2BnC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendOrgInviteEmail = sendOrgInviteEmail;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
const resendClient_1 = require("./resendClient");
|
|
6
|
+
const log = (0, logger_1.createLogger)("email:orgInvite");
|
|
7
|
+
const ROLE_LABEL = {
|
|
8
|
+
owner: "Owner",
|
|
9
|
+
admin: "Admin",
|
|
10
|
+
member: "Member",
|
|
11
|
+
};
|
|
12
|
+
/** Minimal HTML-escaping for the handful of user-controlled strings we interpolate. */
|
|
13
|
+
function esc(s) {
|
|
14
|
+
return s
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
20
|
+
}
|
|
21
|
+
function renderHtml(input) {
|
|
22
|
+
const orgName = esc(input.orgName);
|
|
23
|
+
const inviterName = esc(input.inviterName);
|
|
24
|
+
const roleLabel = ROLE_LABEL[input.role] ?? input.role;
|
|
25
|
+
const acceptUrl = esc(input.acceptUrl);
|
|
26
|
+
// Inline styles only — email clients don't reliably support external/embedded CSS.
|
|
27
|
+
return `<!doctype html>
|
|
28
|
+
<html>
|
|
29
|
+
<body style="margin:0;padding:0;background-color:#f4f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
30
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7;padding:32px 16px;">
|
|
31
|
+
<tr>
|
|
32
|
+
<td align="center">
|
|
33
|
+
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;background-color:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e5ea;">
|
|
34
|
+
<tr>
|
|
35
|
+
<td style="padding:32px 32px 24px 32px;text-align:center;">
|
|
36
|
+
<div style="font-size:20px;font-weight:700;color:#111114;">VidSpotAI</div>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td style="padding:0 32px 8px 32px;">
|
|
41
|
+
<h1 style="margin:0 0 16px 0;font-size:18px;line-height:1.4;color:#111114;">
|
|
42
|
+
${inviterName} invited you to join <strong>${orgName}</strong>
|
|
43
|
+
</h1>
|
|
44
|
+
<p style="margin:0 0 24px 0;font-size:14px;line-height:1.6;color:#52525b;">
|
|
45
|
+
You've been invited to join the <strong>${orgName}</strong> team on VidSpotAI as
|
|
46
|
+
a <strong>${esc(roleLabel)}</strong>. Accept the invite to start collaborating
|
|
47
|
+
on AI-generated video projects together.
|
|
48
|
+
</p>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<td style="padding:0 32px 32px 32px;text-align:center;">
|
|
53
|
+
<a href="${acceptUrl}"
|
|
54
|
+
style="display:inline-block;background:linear-gradient(90deg,#6366f1,#a855f7,#ec4899);color:#ffffff;text-decoration:none;font-size:14px;font-weight:600;padding:12px 28px;border-radius:8px;">
|
|
55
|
+
Accept invite
|
|
56
|
+
</a>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td style="padding:0 32px 32px 32px;">
|
|
61
|
+
<p style="margin:0;font-size:12px;line-height:1.6;color:#a1a1aa;">
|
|
62
|
+
If the button doesn't work, copy and paste this link into your browser:<br />
|
|
63
|
+
<a href="${acceptUrl}" style="color:#6366f1;word-break:break-all;">${acceptUrl}</a>
|
|
64
|
+
</p>
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td style="padding:20px 32px;background-color:#fafafa;border-top:1px solid #e5e5ea;">
|
|
69
|
+
<p style="margin:0;font-size:11px;line-height:1.5;color:#a1a1aa;">
|
|
70
|
+
This invite expires in 14 days. If you weren't expecting this, you can safely
|
|
71
|
+
ignore this email.
|
|
72
|
+
</p>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
</table>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</table>
|
|
79
|
+
</body>
|
|
80
|
+
</html>`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Sends the org-invite email via Resend. Throws on misconfiguration or a
|
|
84
|
+
* Resend-reported failure — this is intentional: callers that want
|
|
85
|
+
* best-effort semantics (the invite doc + copyable link are already the
|
|
86
|
+
* source of truth) should call this WITHOUT awaiting and chain `.catch()` to
|
|
87
|
+
* log rather than fail the parent request. See
|
|
88
|
+
* `organization.controller.ts#createInvite`.
|
|
89
|
+
*/
|
|
90
|
+
async function sendOrgInviteEmail(input) {
|
|
91
|
+
if (!(0, resendClient_1.isResendConfigured)()) {
|
|
92
|
+
throw new Error("sendOrgInviteEmail: RESEND_API_KEY not set");
|
|
93
|
+
}
|
|
94
|
+
const resend = (0, resendClient_1.getResendClient)();
|
|
95
|
+
const subject = `${input.inviterName} invited you to join ${input.orgName} on VidSpotAI`;
|
|
96
|
+
const { data, error } = await resend.emails.send({
|
|
97
|
+
from: resendClient_1.EMAIL_FROM,
|
|
98
|
+
to: input.to,
|
|
99
|
+
subject,
|
|
100
|
+
html: renderHtml(input),
|
|
101
|
+
});
|
|
102
|
+
if (error || !data?.id) {
|
|
103
|
+
const detail = error?.message ?? "no message id returned";
|
|
104
|
+
throw new Error(`sendOrgInviteEmail: Resend send failed — ${detail}`);
|
|
105
|
+
}
|
|
106
|
+
log.info("sendOrgInviteEmail sent", {
|
|
107
|
+
to: input.to,
|
|
108
|
+
orgName: input.orgName,
|
|
109
|
+
role: input.role,
|
|
110
|
+
id: data.id,
|
|
111
|
+
});
|
|
112
|
+
return { id: data.id };
|
|
113
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Resend } from "resend";
|
|
2
|
+
/** True once RESEND_API_KEY is configured. Gate email-sending on this. */
|
|
3
|
+
export declare function isResendConfigured(): boolean;
|
|
4
|
+
/** Lazily-constructed singleton Resend client. Throws if not configured. */
|
|
5
|
+
export declare function getResendClient(): Resend;
|
|
6
|
+
/**
|
|
7
|
+
* Verified sending domain (DKIM/SPF confirmed in Resend for vidspotai.com).
|
|
8
|
+
* Keep the display name generic — individual senders can override the
|
|
9
|
+
* subject/body per email type.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EMAIL_FROM = "VidSpotAI <noreply@vidspotai.com>";
|
|
12
|
+
//# sourceMappingURL=resendClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resendClient.d.ts","sourceRoot":"","sources":["../../../src/services/email/resendClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAgBhC,0EAA0E;AAC1E,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,IAAI,MAAM,CASxC;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,sCAAsC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_FROM = void 0;
|
|
4
|
+
exports.isResendConfigured = isResendConfigured;
|
|
5
|
+
exports.getResendClient = getResendClient;
|
|
6
|
+
const resend_1 = require("resend");
|
|
7
|
+
/**
|
|
8
|
+
* Thin Resend client wrapper. Mirrors the other single-provider wrappers in
|
|
9
|
+
* this codebase (e.g. `services/demo/githubOAuth.ts`): a lazy singleton keyed
|
|
10
|
+
* off an env var, a `isXConfigured()` gate the caller can check before doing
|
|
11
|
+
* real work, and a getter that throws a descriptive error instead of letting
|
|
12
|
+
* the SDK fail on `undefined`.
|
|
13
|
+
*
|
|
14
|
+
* Env:
|
|
15
|
+
* RESEND_API_KEY — Resend API key (Doppler `vidspotai-backend` / `dev`
|
|
16
|
+
* config only for now; NOT set in `prd`).
|
|
17
|
+
*/
|
|
18
|
+
let client = null;
|
|
19
|
+
/** True once RESEND_API_KEY is configured. Gate email-sending on this. */
|
|
20
|
+
function isResendConfigured() {
|
|
21
|
+
return Boolean(process.env.RESEND_API_KEY);
|
|
22
|
+
}
|
|
23
|
+
/** Lazily-constructed singleton Resend client. Throws if not configured. */
|
|
24
|
+
function getResendClient() {
|
|
25
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
throw new Error("resend: RESEND_API_KEY not set");
|
|
28
|
+
}
|
|
29
|
+
if (!client) {
|
|
30
|
+
client = new resend_1.Resend(apiKey);
|
|
31
|
+
}
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Verified sending domain (DKIM/SPF confirmed in Resend for vidspotai.com).
|
|
36
|
+
* Keep the display name generic — individual senders can override the
|
|
37
|
+
* subject/body per email type.
|
|
38
|
+
*/
|
|
39
|
+
exports.EMAIL_FROM = "VidSpotAI <noreply@vidspotai.com>";
|
|
@@ -17,6 +17,7 @@ import { IVideoQaJobModel } from "../models/videoQaJob.model";
|
|
|
17
17
|
import { IGithubConnectionModel } from "../models/githubConnection.model";
|
|
18
18
|
import { IProductConnectionModel } from "../models/productConnection.model";
|
|
19
19
|
import { IOutreachCampaignModel, IOutreachRecipientModel, IOutreachTemplateModel, IPersonalizedVideoModel, IVideoViewModel } from "../models/outreach.model";
|
|
20
|
+
import { IOrganizationCreditLedgerEntry, IOrganizationInviteModel, IOrganizationMemberModel, IOrganizationModel } from "../models/organization.model";
|
|
20
21
|
/**
|
|
21
22
|
* FirestoreService
|
|
22
23
|
* ----------------
|
|
@@ -80,6 +81,10 @@ export declare class FirestoreService {
|
|
|
80
81
|
static outreachCampaignsCol: CollectionReference<IOutreachCampaignModel, DocumentData>;
|
|
81
82
|
static outreachRecipientsCol: CollectionReference<IOutreachRecipientModel, DocumentData>;
|
|
82
83
|
static videoViewsCol: CollectionReference<IVideoViewModel, DocumentData>;
|
|
84
|
+
static organizationsCol: CollectionReference<IOrganizationModel, DocumentData>;
|
|
85
|
+
static organizationMembersCol: CollectionReference<IOrganizationMemberModel, DocumentData>;
|
|
86
|
+
static organizationInvitesCol: CollectionReference<IOrganizationInviteModel, DocumentData>;
|
|
87
|
+
static organizationCreditLedgerCol: CollectionReference<IOrganizationCreditLedgerEntry, DocumentData>;
|
|
83
88
|
/**
|
|
84
89
|
* Sentinel for `.update()` that removes a field from a doc (wraps
|
|
85
90
|
* `FieldValue.delete()`). Use to clear a transient field — e.g. plan-B's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.service.d.ts","sourceRoot":"","sources":["../../src/services/firestore.service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,gBAAgB,EAEhB,aAAa,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EAChB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"firestore.service.d.ts","sourceRoot":"","sources":["../../src/services/firestore.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -217,3 +217,16 @@ FirestoreService.outreachRecipientsCol = firebase_1.firestore
|
|
|
217
217
|
FirestoreService.videoViewsCol = firebase_1.firestore
|
|
218
218
|
.collection("videoViews")
|
|
219
219
|
.withConverter(collectionConverter());
|
|
220
|
+
// Enterprise: Organizations & Team Members
|
|
221
|
+
FirestoreService.organizationsCol = firebase_1.firestore
|
|
222
|
+
.collection("organizations")
|
|
223
|
+
.withConverter(collectionConverter());
|
|
224
|
+
FirestoreService.organizationMembersCol = firebase_1.firestore
|
|
225
|
+
.collection("organizationMembers")
|
|
226
|
+
.withConverter(collectionConverter());
|
|
227
|
+
FirestoreService.organizationInvitesCol = firebase_1.firestore
|
|
228
|
+
.collection("organizationInvites")
|
|
229
|
+
.withConverter(collectionConverter());
|
|
230
|
+
FirestoreService.organizationCreditLedgerCol = firebase_1.firestore
|
|
231
|
+
.collection("organizationCreditLedger")
|
|
232
|
+
.withConverter(collectionConverter());
|
package/lib/services/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,SAAS,CAAC"}
|
package/lib/services/index.js
CHANGED
|
@@ -51,3 +51,5 @@ __exportStar(require("./socialAI"), exports);
|
|
|
51
51
|
__exportStar(require("./socialInsights"), exports);
|
|
52
52
|
__exportStar(require("./socialAccounts"), exports);
|
|
53
53
|
__exportStar(require("./freePlan"), exports);
|
|
54
|
+
__exportStar(require("./organizationCredit.service"), exports);
|
|
55
|
+
__exportStar(require("./email"), exports);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise: Organizations & Team Members — credit-pool funding.
|
|
3
|
+
*
|
|
4
|
+
* Day-to-day spend/refund against the pool happens transparently inside
|
|
5
|
+
* `CreditService.consumeCredits`/`refundCredits` (redirected there when the
|
|
6
|
+
* acting user has `organizationId` set, within the SAME transaction as the
|
|
7
|
+
* personal-credit path would have used — Firestore transactions can't nest,
|
|
8
|
+
* so that logic lives inline in credit.service.ts rather than calling out to
|
|
9
|
+
* this file). This service covers the one op that's genuinely distinct: the
|
|
10
|
+
* owner moving credits FROM their own personal balance INTO the org pool.
|
|
11
|
+
* No new Stripe/seat billing — the pool is funded entirely this way for MVP.
|
|
12
|
+
*/
|
|
13
|
+
export declare class OrganizationCreditService {
|
|
14
|
+
/**
|
|
15
|
+
* Owner-only. Reads owner's user doc + the org doc, then in the same
|
|
16
|
+
* transaction: increments the owner's personal `credits.consumed` (spends
|
|
17
|
+
* `amount` of their own remaining balance) and increments the org's
|
|
18
|
+
* `credits.total` (tops up the pool) by the same amount. Throws
|
|
19
|
+
* "INSUFFICIENT_CREDITS" if the owner doesn't have `amount` remaining —
|
|
20
|
+
* reusing the existing personal error code since this is, from the owner's
|
|
21
|
+
* point of view, spending their own balance.
|
|
22
|
+
*/
|
|
23
|
+
static transferToOrgPool(orgId: string, ownerUid: string, amount: number): Promise<{
|
|
24
|
+
orgTotal: number;
|
|
25
|
+
orgRemaining: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Owner/admin-set monthly draw cap for a member (only meaningful once the
|
|
29
|
+
* org's creditMode is "allocated"). Setting `monthlyTotal` to undefined
|
|
30
|
+
* clears the member's allocation (falls back to full-pool access if the org
|
|
31
|
+
* later flips back to "shared", or is simply uncapped within "allocated"
|
|
32
|
+
* mode until set again).
|
|
33
|
+
*/
|
|
34
|
+
static setMemberAllocation(orgId: string, memberUid: string, monthlyTotal: number | undefined): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=organizationCredit.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"organizationCredit.service.d.ts","sourceRoot":"","sources":["../../src/services/organizationCredit.service.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,qBAAa,yBAAyB;IACpC;;;;;;;;OAQG;WACU,iBAAiB,CAC5B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAqDtD;;;;;;OAMG;WACU,mBAAmB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,IAAI,CAAC;CA0BjB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrganizationCreditService = void 0;
|
|
4
|
+
const firestore_1 = require("firebase-admin/firestore");
|
|
5
|
+
const firebase_1 = require("../libs/firebase");
|
|
6
|
+
const organization_model_1 = require("../models/organization.model");
|
|
7
|
+
const firestore_service_1 = require("./firestore.service");
|
|
8
|
+
/**
|
|
9
|
+
* Enterprise: Organizations & Team Members — credit-pool funding.
|
|
10
|
+
*
|
|
11
|
+
* Day-to-day spend/refund against the pool happens transparently inside
|
|
12
|
+
* `CreditService.consumeCredits`/`refundCredits` (redirected there when the
|
|
13
|
+
* acting user has `organizationId` set, within the SAME transaction as the
|
|
14
|
+
* personal-credit path would have used — Firestore transactions can't nest,
|
|
15
|
+
* so that logic lives inline in credit.service.ts rather than calling out to
|
|
16
|
+
* this file). This service covers the one op that's genuinely distinct: the
|
|
17
|
+
* owner moving credits FROM their own personal balance INTO the org pool.
|
|
18
|
+
* No new Stripe/seat billing — the pool is funded entirely this way for MVP.
|
|
19
|
+
*/
|
|
20
|
+
class OrganizationCreditService {
|
|
21
|
+
/**
|
|
22
|
+
* Owner-only. Reads owner's user doc + the org doc, then in the same
|
|
23
|
+
* transaction: increments the owner's personal `credits.consumed` (spends
|
|
24
|
+
* `amount` of their own remaining balance) and increments the org's
|
|
25
|
+
* `credits.total` (tops up the pool) by the same amount. Throws
|
|
26
|
+
* "INSUFFICIENT_CREDITS" if the owner doesn't have `amount` remaining —
|
|
27
|
+
* reusing the existing personal error code since this is, from the owner's
|
|
28
|
+
* point of view, spending their own balance.
|
|
29
|
+
*/
|
|
30
|
+
static async transferToOrgPool(orgId, ownerUid, amount) {
|
|
31
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
32
|
+
throw new Error("INVALID_AMOUNT");
|
|
33
|
+
}
|
|
34
|
+
return firebase_1.firestore.runTransaction(async (tx) => {
|
|
35
|
+
const userRef = firestore_service_1.FirestoreService.usersCol.doc(ownerUid);
|
|
36
|
+
const orgRef = firestore_service_1.FirestoreService.organizationsCol.doc(orgId);
|
|
37
|
+
// All reads before any writes (Firestore transaction requirement).
|
|
38
|
+
const [userSnap, orgSnap] = await Promise.all([
|
|
39
|
+
tx.get(userRef),
|
|
40
|
+
tx.get(orgRef),
|
|
41
|
+
]);
|
|
42
|
+
if (!userSnap.exists)
|
|
43
|
+
throw new Error("USER_NOT_FOUND");
|
|
44
|
+
if (!orgSnap.exists)
|
|
45
|
+
throw new Error("ORG_NOT_FOUND");
|
|
46
|
+
const userData = userSnap.data();
|
|
47
|
+
const orgData = orgSnap.data();
|
|
48
|
+
if (orgData?.ownerId !== ownerUid) {
|
|
49
|
+
throw new Error("ORG_NOT_OWNER");
|
|
50
|
+
}
|
|
51
|
+
const userConsumed = userData?.credits?.consumed ?? 0;
|
|
52
|
+
const userTotal = userData?.credits?.total ?? 0;
|
|
53
|
+
if (userConsumed + amount > userTotal) {
|
|
54
|
+
throw new Error("INSUFFICIENT_CREDITS");
|
|
55
|
+
}
|
|
56
|
+
const orgConsumed = orgData?.credits?.consumed ?? 0;
|
|
57
|
+
const orgTotal = orgData?.credits?.total ?? 0;
|
|
58
|
+
const newOrgTotal = orgTotal + amount;
|
|
59
|
+
tx.update(userRef, { "credits.consumed": userConsumed + amount });
|
|
60
|
+
tx.update(orgRef, { "credits.total": newOrgTotal });
|
|
61
|
+
const ledgerRef = firestore_service_1.FirestoreService.organizationCreditLedgerCol.doc();
|
|
62
|
+
tx.set(ledgerRef, {
|
|
63
|
+
id: ledgerRef.id,
|
|
64
|
+
orgId,
|
|
65
|
+
actorUid: ownerUid,
|
|
66
|
+
delta: amount,
|
|
67
|
+
reason: "transfer_in",
|
|
68
|
+
balanceAfter: newOrgTotal - orgConsumed,
|
|
69
|
+
createdAt: firestore_1.FieldValue.serverTimestamp(),
|
|
70
|
+
});
|
|
71
|
+
return { orgTotal: newOrgTotal, orgRemaining: newOrgTotal - orgConsumed };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Owner/admin-set monthly draw cap for a member (only meaningful once the
|
|
76
|
+
* org's creditMode is "allocated"). Setting `monthlyTotal` to undefined
|
|
77
|
+
* clears the member's allocation (falls back to full-pool access if the org
|
|
78
|
+
* later flips back to "shared", or is simply uncapped within "allocated"
|
|
79
|
+
* mode until set again).
|
|
80
|
+
*/
|
|
81
|
+
static async setMemberAllocation(orgId, memberUid, monthlyTotal) {
|
|
82
|
+
const memberRef = firestore_service_1.FirestoreService.organizationMembersCol.doc((0, organization_model_1.organizationMemberDocId)(orgId, memberUid));
|
|
83
|
+
const snap = await memberRef.get();
|
|
84
|
+
if (!snap.exists)
|
|
85
|
+
throw new Error("ORG_MEMBER_NOT_FOUND");
|
|
86
|
+
if (monthlyTotal === undefined) {
|
|
87
|
+
await memberRef.update({
|
|
88
|
+
monthlyAllocation: firestore_service_1.FirestoreService.deleteField(),
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isFinite(monthlyTotal) || monthlyTotal < 0) {
|
|
93
|
+
throw new Error("INVALID_AMOUNT");
|
|
94
|
+
}
|
|
95
|
+
const existing = snap.data()?.monthlyAllocation;
|
|
96
|
+
await memberRef.update({
|
|
97
|
+
monthlyAllocation: {
|
|
98
|
+
total: monthlyTotal,
|
|
99
|
+
consumed: existing?.consumed ?? 0,
|
|
100
|
+
resetAt: existing?.resetAt ?? Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.OrganizationCreditService = OrganizationCreditService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAqGvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;
|
|
1
|
+
{"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAqGvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAiCD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
|