@fabriktor/fx 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/billable/billable.d.ts +12 -10
- package/dist/src/billable/billable.js +1 -2
- package/dist/src/calls/calls.d.ts +5 -5
- package/dist/src/chat/chat.d.ts +3 -2
- package/dist/src/core/pull.d.ts +39 -0
- package/dist/src/core/pull.js +101 -0
- package/dist/src/feed/feed.d.ts +167 -0
- package/dist/src/feed/feed.js +642 -0
- package/dist/src/fx.d.ts +42 -1
- package/dist/src/fx.js +94 -2
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/jobs/jobs.d.ts +74 -0
- package/dist/src/jobs/jobs.js +375 -0
- package/dist/src/marketplace/marketplace.d.ts +35 -0
- package/dist/src/marketplace/marketplace.js +159 -0
- package/dist/src/notifications/notifications.d.ts +20 -0
- package/dist/src/notifications/notifications.js +45 -0
- package/dist/src/payments/payments.d.ts +59 -0
- package/dist/src/payments/payments.js +266 -0
- package/dist/src/payrolls/payrolls.d.ts +52 -0
- package/dist/src/payrolls/payrolls.js +54 -0
- package/dist/src/privacy/privacy.d.ts +21 -0
- package/dist/src/privacy/privacy.js +78 -0
- package/dist/src/routes/routes.d.ts +22 -0
- package/dist/src/routes/routes.js +134 -0
- package/dist/src/session/session.d.ts +17 -11
- package/dist/src/session/session.js +6 -12
- package/dist/src/storage/storage.d.ts +22 -13
- package/dist/src/storage/storage.js +56 -41
- package/dist/src/timesheets/timesheets.d.ts +91 -0
- package/dist/src/timesheets/timesheets.js +407 -0
- package/dist/src/users/users.d.ts +15 -11
- package/dist/src/users/users.js +2 -2
- package/package.json +3 -3
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// Copyright (C) Fabriktor, Inc. 2025-present.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
// not use this file except in compliance with the License. You may obtain
|
|
5
|
+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
import { LocationTypePersistent, Pending, Started, Terminated, ZeroID, } from "@fabriktor/schema";
|
|
7
|
+
import { errRequired, errValidation } from "../core/err.js";
|
|
8
|
+
const msgJobIDRequired = "Job ID is required.";
|
|
9
|
+
const msgLocationIDRequired = "Location ID is required.";
|
|
10
|
+
const msgMemberIDRequired = "Member ID is required.";
|
|
11
|
+
const msgJobLocationMismatch = "Job location does not match the provided parent location.";
|
|
12
|
+
const msgJobMembersOutsideLocation = "All job members must belong to the parent location.";
|
|
13
|
+
const msgProgramMembersOutsideLocation = "All program job members must belong to the parent location.";
|
|
14
|
+
const msgForemanOutsideLocation = "Foreman must belong to the parent location.";
|
|
15
|
+
const msgJobStartPast = "A past-dated job must be explicitly created as an immediate job.";
|
|
16
|
+
const msgProgramStartPast = "Program start date must not be in the past.";
|
|
17
|
+
const msgJobStartOutsideLocation = "Job start date must fall within the parent location time span.";
|
|
18
|
+
const msgProgramStartOutsideLocation = "Program job start date must fall within the parent location time span.";
|
|
19
|
+
const msgLocationMemberAssigned = "Cannot remove a location member while they are assigned to a non-terminated job in that location.";
|
|
20
|
+
const msgDateInvalid = "Date contains an invalid UTC value.";
|
|
21
|
+
const msgLocationRangeInvalid = "Location contains an invalid scheduled time span.";
|
|
22
|
+
const msgTimezoneInvalid = "Program timezone is invalid.";
|
|
23
|
+
export const JobProgramCreateMode = {
|
|
24
|
+
Scheduled: "scheduled",
|
|
25
|
+
Immediate: "immediate",
|
|
26
|
+
};
|
|
27
|
+
export class JobsFX {
|
|
28
|
+
jobs;
|
|
29
|
+
now;
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.jobs = opts.jobs;
|
|
32
|
+
this.now = opts.now ?? (() => new globalThis.Date());
|
|
33
|
+
}
|
|
34
|
+
async createJob(opts) {
|
|
35
|
+
const location_id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
36
|
+
const foremen = normalizeIDs(opts.in.foremen);
|
|
37
|
+
const current_members = unionIDs(opts.in.current_members, foremen);
|
|
38
|
+
validateLocationMembers({
|
|
39
|
+
members: current_members,
|
|
40
|
+
location: opts.location,
|
|
41
|
+
field: "in",
|
|
42
|
+
msg: msgJobMembersOutsideLocation,
|
|
43
|
+
});
|
|
44
|
+
validateJobStart(opts, this.now());
|
|
45
|
+
const input = {
|
|
46
|
+
...opts.in,
|
|
47
|
+
status: opts.start_immediately ? Started : Pending,
|
|
48
|
+
is_active: opts.start_immediately,
|
|
49
|
+
auto_start: opts.start_immediately ? true : opts.auto_start,
|
|
50
|
+
all_members: [],
|
|
51
|
+
current_members,
|
|
52
|
+
foremen,
|
|
53
|
+
location_id,
|
|
54
|
+
recurrent: false,
|
|
55
|
+
};
|
|
56
|
+
return await this.jobs.insertOneJob({
|
|
57
|
+
in: input,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async createJobProgram(opts) {
|
|
61
|
+
const job_location_id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
62
|
+
const job_foremen = normalizeIDs(opts.in.job_foremen);
|
|
63
|
+
const job_current_members = unionIDs(opts.in.job_current_members, job_foremen);
|
|
64
|
+
validateLocationMembers({
|
|
65
|
+
members: job_current_members,
|
|
66
|
+
location: opts.location,
|
|
67
|
+
field: "in",
|
|
68
|
+
msg: msgProgramMembersOutsideLocation,
|
|
69
|
+
});
|
|
70
|
+
const input = {
|
|
71
|
+
...opts.in,
|
|
72
|
+
job_current_members,
|
|
73
|
+
job_foremen,
|
|
74
|
+
job_location_id,
|
|
75
|
+
current_occurrences: 0,
|
|
76
|
+
};
|
|
77
|
+
const now = this.now();
|
|
78
|
+
const current_day = isCurrentProgramDay(input, now);
|
|
79
|
+
validateProgramStart({
|
|
80
|
+
input,
|
|
81
|
+
location: opts.location,
|
|
82
|
+
now,
|
|
83
|
+
current_day,
|
|
84
|
+
});
|
|
85
|
+
if (current_day) {
|
|
86
|
+
await this.jobs.immediateJobFromProgram(input);
|
|
87
|
+
return {
|
|
88
|
+
mode: JobProgramCreateMode.Immediate,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const create = await this.jobs.insertOneJobProgram({
|
|
92
|
+
in: input,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
mode: JobProgramCreateMode.Scheduled,
|
|
96
|
+
create,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async findLocationOverlaps(opts) {
|
|
100
|
+
const input = {
|
|
101
|
+
start_date: {
|
|
102
|
+
y: opts.in.y,
|
|
103
|
+
m: opts.in.m,
|
|
104
|
+
d: opts.in.d,
|
|
105
|
+
h: opts.in.h,
|
|
106
|
+
min: opts.in.min,
|
|
107
|
+
tz: opts.in.tz,
|
|
108
|
+
},
|
|
109
|
+
end_date: {
|
|
110
|
+
y: opts.in.end_date.y,
|
|
111
|
+
m: opts.in.end_date.m,
|
|
112
|
+
d: opts.in.end_date.d,
|
|
113
|
+
h: opts.in.end_date.h,
|
|
114
|
+
min: opts.in.end_date.min,
|
|
115
|
+
tz: opts.in.end_date.tz,
|
|
116
|
+
},
|
|
117
|
+
source_id: resolvedID(opts.source_id) ?? ZeroID,
|
|
118
|
+
exclude_pending: opts.exclude_pending ?? false,
|
|
119
|
+
exclude_started: opts.exclude_started ?? false,
|
|
120
|
+
exclude_terminated: opts.exclude_terminated ?? false,
|
|
121
|
+
};
|
|
122
|
+
return await this.jobs.findOverlaps(input);
|
|
123
|
+
}
|
|
124
|
+
async addJobForeman(opts) {
|
|
125
|
+
const id = requiredID(opts.job.id, "job", msgJobIDRequired);
|
|
126
|
+
const location_id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
127
|
+
const job_location_id = opts.job.location_id.trim();
|
|
128
|
+
if (job_location_id !== location_id) {
|
|
129
|
+
throw errValidation({
|
|
130
|
+
field: "job",
|
|
131
|
+
msg: msgJobLocationMismatch,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const member_id = opts.member_id.trim();
|
|
135
|
+
if (member_id === "") {
|
|
136
|
+
throw errRequired({
|
|
137
|
+
field: "member_id",
|
|
138
|
+
msg: msgMemberIDRequired,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const location_members = new Set(normalizeIDs(opts.location.current_members));
|
|
142
|
+
if (!location_members.has(member_id)) {
|
|
143
|
+
throw errValidation({
|
|
144
|
+
field: "member_id",
|
|
145
|
+
msg: msgForemanOutsideLocation,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return await this.jobs.replaceOneJob({
|
|
149
|
+
id,
|
|
150
|
+
in: {
|
|
151
|
+
current_members: unionIDs(opts.job.current_members, [member_id]),
|
|
152
|
+
foremen: unionIDs(opts.job.foremen, [member_id]),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async removeJobMember(opts) {
|
|
157
|
+
const id = requiredID(opts.job.id, "job", msgJobIDRequired);
|
|
158
|
+
const member_id = opts.member_id.trim();
|
|
159
|
+
if (member_id === "") {
|
|
160
|
+
throw errRequired({
|
|
161
|
+
field: "member_id",
|
|
162
|
+
msg: msgMemberIDRequired,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return await this.jobs.replaceOneJob({
|
|
166
|
+
id,
|
|
167
|
+
in: {
|
|
168
|
+
current_members: withoutID(opts.job.current_members, member_id),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async setLocationMembers(opts) {
|
|
173
|
+
const id = requiredID(opts.location.id, "location", msgLocationIDRequired);
|
|
174
|
+
const current_members = normalizeIDs(opts.current_members);
|
|
175
|
+
const next = new Set(current_members);
|
|
176
|
+
const removed = normalizeIDs(opts.location.current_members).filter((member_id) => !next.has(member_id));
|
|
177
|
+
if (removed.length > 0) {
|
|
178
|
+
const out = await this.jobs.queryJobs({
|
|
179
|
+
query: {
|
|
180
|
+
location_id: id,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const removed_set = new Set(removed);
|
|
184
|
+
for (const job of out.value ?? []) {
|
|
185
|
+
if (job.status === Terminated) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (normalizeIDs(job.current_members).some((member_id) => removed_set.has(member_id))) {
|
|
189
|
+
throw errValidation({
|
|
190
|
+
field: "current_members",
|
|
191
|
+
msg: msgLocationMemberAssigned,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return await this.jobs.replaceOneLocation({
|
|
197
|
+
id,
|
|
198
|
+
in: {
|
|
199
|
+
current_members,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function newJobsFX(opts) {
|
|
205
|
+
return new JobsFX(opts);
|
|
206
|
+
}
|
|
207
|
+
function validateLocationMembers(opts) {
|
|
208
|
+
const allowed = new Set(normalizeIDs(opts.location.current_members));
|
|
209
|
+
if (opts.members.some((member_id) => !allowed.has(member_id))) {
|
|
210
|
+
throw errValidation({
|
|
211
|
+
field: opts.field,
|
|
212
|
+
msg: opts.msg,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function validateJobStart(opts, now) {
|
|
217
|
+
const start_ms = utcMilliseconds(opts.in.utc, "in");
|
|
218
|
+
if (!opts.start_immediately && start_ms < now.getTime()) {
|
|
219
|
+
throw errValidation({
|
|
220
|
+
field: "in",
|
|
221
|
+
msg: msgJobStartPast,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
validateLocationStart({
|
|
225
|
+
start_ms,
|
|
226
|
+
location: opts.location,
|
|
227
|
+
field: "in",
|
|
228
|
+
msg: msgJobStartOutsideLocation,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function validateProgramStart(opts) {
|
|
232
|
+
const start_ms = utcMilliseconds(opts.input.job_start_date.utc, "in");
|
|
233
|
+
if (!opts.current_day && start_ms < opts.now.getTime()) {
|
|
234
|
+
throw errValidation({
|
|
235
|
+
field: "in",
|
|
236
|
+
msg: msgProgramStartPast,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
validateLocationStart({
|
|
240
|
+
start_ms,
|
|
241
|
+
location: opts.location,
|
|
242
|
+
field: "in",
|
|
243
|
+
msg: msgProgramStartOutsideLocation,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function validateLocationStart(opts) {
|
|
247
|
+
if (opts.location.type === LocationTypePersistent) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const location_start_ms = parsedUTC(opts.location.utc);
|
|
251
|
+
const location_end_ms = parsedUTC(opts.location.end_date.utc);
|
|
252
|
+
if (location_start_ms === undefined ||
|
|
253
|
+
location_end_ms === undefined ||
|
|
254
|
+
location_end_ms < location_start_ms) {
|
|
255
|
+
throw errValidation({
|
|
256
|
+
field: opts.field,
|
|
257
|
+
msg: msgLocationRangeInvalid,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (opts.start_ms < location_start_ms || opts.start_ms > location_end_ms) {
|
|
261
|
+
throw errValidation({
|
|
262
|
+
field: opts.field,
|
|
263
|
+
msg: opts.msg,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function isCurrentProgramDay(input, now) {
|
|
268
|
+
const day = localDayParts(now, input.timezone);
|
|
269
|
+
if (day === undefined) {
|
|
270
|
+
throw errValidation({
|
|
271
|
+
field: "in",
|
|
272
|
+
msg: msgTimezoneInvalid,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return (input.job_start_date.y === day.y &&
|
|
276
|
+
input.job_start_date.m === day.m &&
|
|
277
|
+
input.job_start_date.d === day.d);
|
|
278
|
+
}
|
|
279
|
+
function localDayParts(now, timezone) {
|
|
280
|
+
const time_zone = timezone.trim();
|
|
281
|
+
if (time_zone === "") {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
let parts;
|
|
285
|
+
try {
|
|
286
|
+
parts = new Intl.DateTimeFormat("en-CA", {
|
|
287
|
+
timeZone: time_zone,
|
|
288
|
+
year: "numeric",
|
|
289
|
+
month: "numeric",
|
|
290
|
+
day: "numeric",
|
|
291
|
+
}).formatToParts(now);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const y = datePart(parts, "year");
|
|
297
|
+
const m = datePart(parts, "month");
|
|
298
|
+
const d = datePart(parts, "day");
|
|
299
|
+
if (y === undefined || m === undefined || d === undefined) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
y,
|
|
304
|
+
m,
|
|
305
|
+
d,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function datePart(parts, type) {
|
|
309
|
+
const raw = parts.find((part) => part.type === type)?.value;
|
|
310
|
+
if (raw === undefined) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const out = Number.parseInt(raw, 10);
|
|
314
|
+
if (!Number.isFinite(out)) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
function utcMilliseconds(v, field) {
|
|
320
|
+
const out = parsedUTC(v);
|
|
321
|
+
if (out === undefined) {
|
|
322
|
+
throw errValidation({
|
|
323
|
+
field,
|
|
324
|
+
msg: msgDateInvalid,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function parsedUTC(v) {
|
|
330
|
+
const raw = v.trim();
|
|
331
|
+
if (raw === "") {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
const out = globalThis.Date.parse(raw);
|
|
335
|
+
if (!Number.isFinite(out)) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
function requiredID(v, field, msg) {
|
|
341
|
+
const out = resolvedID(v);
|
|
342
|
+
if (out === undefined) {
|
|
343
|
+
throw errRequired({
|
|
344
|
+
field,
|
|
345
|
+
msg,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function resolvedID(v) {
|
|
351
|
+
const out = v?.trim();
|
|
352
|
+
if (out === undefined || out === "" || out === ZeroID) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
function normalizeIDs(ids) {
|
|
358
|
+
const seen = new Set();
|
|
359
|
+
const out = [];
|
|
360
|
+
for (const raw of ids) {
|
|
361
|
+
const id = raw.trim();
|
|
362
|
+
if (id === "" || seen.has(id)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
seen.add(id);
|
|
366
|
+
out.push(id);
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
function unionIDs(a, b) {
|
|
371
|
+
return normalizeIDs([...a, ...b]);
|
|
372
|
+
}
|
|
373
|
+
function withoutID(ids, removed_id) {
|
|
374
|
+
return normalizeIDs(ids).filter((id) => id !== removed_id);
|
|
375
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RPMarketplacePull } from "@fabriktor/schema";
|
|
2
|
+
import type { MarketplaceOperator, OperationResultOf } from "@fabriktor/client";
|
|
3
|
+
import { type PullSession, type PullSessionErrorHandler } from "../core/pull.js";
|
|
4
|
+
type MarketplacePullOptions = Parameters<MarketplaceOperator["pull"]>[0];
|
|
5
|
+
export type MarketplacePullContext = Pick<MarketplacePullOptions, "pull_type" | "entity_id" | "research" | "research_user_type">;
|
|
6
|
+
export type MarketplacePullResult = OperationResultOf<RPMarketplacePull>;
|
|
7
|
+
export type MarketplaceResearchUserType = MarketplacePullOptions["research_user_type"];
|
|
8
|
+
export type MarketplaceSession = PullSession<MarketplacePullResult>;
|
|
9
|
+
export type MarketplaceSessionErrorHandler = PullSessionErrorHandler;
|
|
10
|
+
export type MarketplaceFXOptions = {
|
|
11
|
+
marketplace: MarketplaceOperator;
|
|
12
|
+
keep_alive_interval_ms: number;
|
|
13
|
+
};
|
|
14
|
+
export type StartMarketplaceSessionOptions = MarketplacePullContext & {
|
|
15
|
+
on_error?: MarketplaceSessionErrorHandler;
|
|
16
|
+
};
|
|
17
|
+
export type StartMarketplaceWelcomeSessionOptions = {
|
|
18
|
+
country: string;
|
|
19
|
+
state?: string;
|
|
20
|
+
research_user_type: MarketplaceResearchUserType;
|
|
21
|
+
on_error?: MarketplaceSessionErrorHandler;
|
|
22
|
+
};
|
|
23
|
+
export interface MarketplaceFXOperator {
|
|
24
|
+
startSession(opts: StartMarketplaceSessionOptions): Promise<MarketplaceSession>;
|
|
25
|
+
startWelcomeSession(opts: StartMarketplaceWelcomeSessionOptions): Promise<MarketplaceSession>;
|
|
26
|
+
}
|
|
27
|
+
export declare class MarketplaceFX implements MarketplaceFXOperator {
|
|
28
|
+
private marketplace;
|
|
29
|
+
private keep_alive_interval_ms;
|
|
30
|
+
constructor(opts: MarketplaceFXOptions);
|
|
31
|
+
startSession(opts: StartMarketplaceSessionOptions): Promise<MarketplaceSession>;
|
|
32
|
+
startWelcomeSession(opts: StartMarketplaceWelcomeSessionOptions): Promise<MarketplaceSession>;
|
|
33
|
+
}
|
|
34
|
+
export declare function newMarketplaceFX(opts: MarketplaceFXOptions): MarketplaceFX;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Copyright (C) Fabriktor, Inc. 2025-present.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
// not use this file except in compliance with the License. You may obtain
|
|
5
|
+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
import { PullTypeBatch, PullTypeKeepAlive, PullTypeResearch, } from "@fabriktor/schema";
|
|
7
|
+
import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
|
|
8
|
+
import { newPullSession, } from "../core/pull.js";
|
|
9
|
+
const newMarketplaceSessionToken = "";
|
|
10
|
+
const resourceMarketplaceSessionToken = "marketplace_session_token";
|
|
11
|
+
const msgKeepAliveIntervalInvalid = "Keep-alive interval must be a finite number greater than zero.";
|
|
12
|
+
const msgMarketplaceSessionClosed = "Marketplace session is closed.";
|
|
13
|
+
const msgMarketplacePullInProgress = "A marketplace pull is already in progress.";
|
|
14
|
+
const msgMarketplaceSessionTokenMissing = "Marketplace pull response did not contain a session token.";
|
|
15
|
+
const msgMarketplaceSessionPullTypeInvalid = "Marketplace sessions only support batch and research pull types.";
|
|
16
|
+
const msgMarketplaceEntityIDRequired = "Entity ID is required for a batch marketplace session.";
|
|
17
|
+
const msgMarketplaceResearchRequired = "Research is required for a research marketplace session.";
|
|
18
|
+
const msgMarketplaceCountryRequired = "Country is required for marketplace welcome research.";
|
|
19
|
+
export class MarketplaceFX {
|
|
20
|
+
marketplace;
|
|
21
|
+
keep_alive_interval_ms;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
validateMarketplaceFXOptions(opts);
|
|
24
|
+
this.marketplace = opts.marketplace;
|
|
25
|
+
this.keep_alive_interval_ms = opts.keep_alive_interval_ms;
|
|
26
|
+
}
|
|
27
|
+
async startSession(opts) {
|
|
28
|
+
const { on_error, ...input } = opts;
|
|
29
|
+
const context = marketplaceSessionContext(input);
|
|
30
|
+
const initial = await this.marketplace.pull({
|
|
31
|
+
...context,
|
|
32
|
+
session_token: newMarketplaceSessionToken,
|
|
33
|
+
is_new_session: true,
|
|
34
|
+
});
|
|
35
|
+
const session_token = marketplaceSessionToken(initial);
|
|
36
|
+
return newPullSession({
|
|
37
|
+
initial,
|
|
38
|
+
session_token,
|
|
39
|
+
keep_alive_interval_ms: this.keep_alive_interval_ms,
|
|
40
|
+
pull_next: async (token) => {
|
|
41
|
+
return await this.marketplace.pull({
|
|
42
|
+
...context,
|
|
43
|
+
session_token: token,
|
|
44
|
+
is_new_session: false,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
keep_alive: async (token) => {
|
|
48
|
+
return await this.marketplace.pull({
|
|
49
|
+
session_token: token,
|
|
50
|
+
is_new_session: false,
|
|
51
|
+
pull_type: PullTypeKeepAlive,
|
|
52
|
+
entity_id: "",
|
|
53
|
+
research: "",
|
|
54
|
+
research_user_type: "",
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
resolve_session_token: marketplaceSessionToken,
|
|
58
|
+
closed_error: () => errValidation({
|
|
59
|
+
msg: msgMarketplaceSessionClosed,
|
|
60
|
+
}),
|
|
61
|
+
pull_in_progress_error: () => errValidation({
|
|
62
|
+
msg: msgMarketplacePullInProgress,
|
|
63
|
+
}),
|
|
64
|
+
on_error,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async startWelcomeSession(opts) {
|
|
68
|
+
const research = marketplaceLocationResearch(opts);
|
|
69
|
+
return await this.startSession({
|
|
70
|
+
pull_type: PullTypeResearch,
|
|
71
|
+
entity_id: "",
|
|
72
|
+
research,
|
|
73
|
+
research_user_type: opts.research_user_type,
|
|
74
|
+
on_error: opts.on_error,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function newMarketplaceFX(opts) {
|
|
79
|
+
return new MarketplaceFX(opts);
|
|
80
|
+
}
|
|
81
|
+
function validateMarketplaceFXOptions(opts) {
|
|
82
|
+
if (!Number.isFinite(opts.keep_alive_interval_ms) ||
|
|
83
|
+
opts.keep_alive_interval_ms <= 0) {
|
|
84
|
+
throw errValidation({
|
|
85
|
+
field: "keep_alive_interval_ms",
|
|
86
|
+
msg: msgKeepAliveIntervalInvalid,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function marketplaceSessionContext(opts) {
|
|
91
|
+
switch (opts.pull_type) {
|
|
92
|
+
case PullTypeBatch: {
|
|
93
|
+
const entity_id = opts.entity_id.trim();
|
|
94
|
+
if (entity_id === "") {
|
|
95
|
+
throw errRequired({
|
|
96
|
+
field: "entity_id",
|
|
97
|
+
msg: msgMarketplaceEntityIDRequired,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
pull_type: PullTypeBatch,
|
|
102
|
+
entity_id,
|
|
103
|
+
research: "",
|
|
104
|
+
research_user_type: "",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
case PullTypeResearch: {
|
|
108
|
+
const research = opts.research.trim();
|
|
109
|
+
if (research === "") {
|
|
110
|
+
throw errRequired({
|
|
111
|
+
field: "research",
|
|
112
|
+
msg: msgMarketplaceResearchRequired,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
pull_type: PullTypeResearch,
|
|
117
|
+
entity_id: "",
|
|
118
|
+
research,
|
|
119
|
+
research_user_type: opts.research_user_type,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
throw errValidation({
|
|
124
|
+
field: "pull_type",
|
|
125
|
+
msg: msgMarketplaceSessionPullTypeInvalid,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function marketplaceLocationResearch(opts) {
|
|
130
|
+
const country = opts.country.trim();
|
|
131
|
+
if (country === "") {
|
|
132
|
+
throw errRequired({
|
|
133
|
+
field: "country",
|
|
134
|
+
msg: msgMarketplaceCountryRequired,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const state = opts.state?.trim();
|
|
138
|
+
if (state === undefined || state === "") {
|
|
139
|
+
return country;
|
|
140
|
+
}
|
|
141
|
+
return `${state}, ${country}`;
|
|
142
|
+
}
|
|
143
|
+
function marketplaceSessionToken(out) {
|
|
144
|
+
const token = resolvedValue(out.value?.session_token);
|
|
145
|
+
if (token === undefined) {
|
|
146
|
+
throw errResolveFailed({
|
|
147
|
+
resource: resourceMarketplaceSessionToken,
|
|
148
|
+
msg: msgMarketplaceSessionTokenMissing,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return token;
|
|
152
|
+
}
|
|
153
|
+
function resolvedValue(v) {
|
|
154
|
+
const out = v?.trim();
|
|
155
|
+
if (out === undefined || out.length === 0) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NotificationsOperator } from "@fabriktor/client";
|
|
2
|
+
export type DeviceTokenProvider = () => Promise<string>;
|
|
3
|
+
export type NotificationsFXOptions = {
|
|
4
|
+
notifications: NotificationsOperator;
|
|
5
|
+
get_device_token: DeviceTokenProvider;
|
|
6
|
+
};
|
|
7
|
+
export type SyncDeviceTokenOptions = {
|
|
8
|
+
owner_id: string;
|
|
9
|
+
};
|
|
10
|
+
export type SyncDeviceTokenResult = Awaited<ReturnType<NotificationsOperator["upsertOneDTok"]>>;
|
|
11
|
+
export interface NotificationsFXOperator {
|
|
12
|
+
syncDeviceToken(opts: SyncDeviceTokenOptions): Promise<SyncDeviceTokenResult>;
|
|
13
|
+
}
|
|
14
|
+
export declare class NotificationsFX implements NotificationsFXOperator {
|
|
15
|
+
private notifications;
|
|
16
|
+
private getDeviceToken;
|
|
17
|
+
constructor(opts: NotificationsFXOptions);
|
|
18
|
+
syncDeviceToken(opts: SyncDeviceTokenOptions): Promise<SyncDeviceTokenResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare function newNotificationsFX(opts: NotificationsFXOptions): NotificationsFX;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (C) Fabriktor, Inc. 2025-present.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
// not use this file except in compliance with the License. You may obtain
|
|
5
|
+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
import { ZeroID } from "@fabriktor/schema";
|
|
7
|
+
import { errRequired } from "../core/err.js";
|
|
8
|
+
const msgOwnerIDRequired = "Owner ID is required.";
|
|
9
|
+
const msgDeviceTokenRequired = "Device token is required.";
|
|
10
|
+
export class NotificationsFX {
|
|
11
|
+
notifications;
|
|
12
|
+
getDeviceToken;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.notifications = opts.notifications;
|
|
15
|
+
this.getDeviceToken = opts.get_device_token;
|
|
16
|
+
}
|
|
17
|
+
async syncDeviceToken(opts) {
|
|
18
|
+
const owner_id = opts.owner_id.trim();
|
|
19
|
+
if (owner_id === "" || owner_id === ZeroID) {
|
|
20
|
+
throw errRequired({
|
|
21
|
+
field: "owner_id",
|
|
22
|
+
msg: msgOwnerIDRequired,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const token = (await this.getDeviceToken()).trim();
|
|
26
|
+
if (token === "") {
|
|
27
|
+
throw errRequired({
|
|
28
|
+
field: "token",
|
|
29
|
+
msg: msgDeviceTokenRequired,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const input = {
|
|
33
|
+
in: {
|
|
34
|
+
token,
|
|
35
|
+
},
|
|
36
|
+
query: {
|
|
37
|
+
owner_id,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return await this.notifications.upsertOneDTok(input);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function newNotificationsFX(opts) {
|
|
44
|
+
return new NotificationsFX(opts);
|
|
45
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Payment, type PmtPublicSession } from "@fabriktor/schema";
|
|
2
|
+
import type { PaymentsOperator } from "@fabriktor/client";
|
|
3
|
+
type PurchaseCreditsPayload = Parameters<PaymentsOperator["purchaseCredits"]>[0];
|
|
4
|
+
type OpenPublicSessionPayload = Parameters<PaymentsOperator["openPmtPublicSession"]>[0];
|
|
5
|
+
type PaymentFromPublicSessionPayload = Parameters<PaymentsOperator["paymentFromPublicSession"]>[0];
|
|
6
|
+
export declare const PublicPaymentSessionStatus: {
|
|
7
|
+
readonly NotFound: "not_found";
|
|
8
|
+
readonly Ready: "ready";
|
|
9
|
+
readonly InternalClient: "internal_client";
|
|
10
|
+
readonly Consumed: "consumed";
|
|
11
|
+
};
|
|
12
|
+
export type PublicPaymentSessionStatus = (typeof PublicPaymentSessionStatus)[keyof typeof PublicPaymentSessionStatus];
|
|
13
|
+
export type PurchaseAppleCreditsOptions = Pick<PurchaseCreditsPayload, "pkg" | "iap_apple">;
|
|
14
|
+
export type PurchaseGoogleCreditsOptions = Pick<PurchaseCreditsPayload, "pkg" | "iap_google">;
|
|
15
|
+
export type PrepareStripeCreditPurchaseOptions = Pick<PurchaseCreditsPayload, "pkg" | "payer_payment_method_psp_id" | "return_url">;
|
|
16
|
+
export type ConfirmStripeCreditPurchaseOptions = {
|
|
17
|
+
payment: Payment;
|
|
18
|
+
};
|
|
19
|
+
export type OpenDirectPublicPaymentSessionOptions = Pick<OpenPublicSessionPayload, "entity_id" | "type">;
|
|
20
|
+
export type OpenLookupPublicPaymentSessionOptions = Pick<OpenPublicSessionPayload, "username" | "type" | "entity_number" | "amount">;
|
|
21
|
+
export type PayPublicSessionOptions = Omit<PaymentFromPublicSessionPayload, "pmt_public_session_id"> & {
|
|
22
|
+
session: PmtPublicSession;
|
|
23
|
+
};
|
|
24
|
+
export type PrepareStripeCreditPurchaseResult = Payment;
|
|
25
|
+
export type ConfirmStripeCreditPurchaseResult = Awaited<ReturnType<PaymentsOperator["replaceOnePayment"]>>;
|
|
26
|
+
export type PayPublicSessionResult = Awaited<ReturnType<PaymentsOperator["paymentFromPublicSession"]>>;
|
|
27
|
+
export type OpenPublicPaymentSessionResult = {
|
|
28
|
+
status: typeof PublicPaymentSessionStatus.NotFound;
|
|
29
|
+
} | {
|
|
30
|
+
status: typeof PublicPaymentSessionStatus.Ready | typeof PublicPaymentSessionStatus.InternalClient | typeof PublicPaymentSessionStatus.Consumed;
|
|
31
|
+
session: PmtPublicSession;
|
|
32
|
+
};
|
|
33
|
+
export type PaymentsFXOptions = {
|
|
34
|
+
payments: PaymentsOperator;
|
|
35
|
+
};
|
|
36
|
+
export interface PaymentsFXOperator {
|
|
37
|
+
purchaseAppleCredits(opts: PurchaseAppleCreditsOptions): Promise<void>;
|
|
38
|
+
purchaseGoogleCredits(opts: PurchaseGoogleCreditsOptions): Promise<void>;
|
|
39
|
+
prepareStripeCreditPurchase(opts: PrepareStripeCreditPurchaseOptions): Promise<PrepareStripeCreditPurchaseResult>;
|
|
40
|
+
confirmStripeCreditPurchase(opts: ConfirmStripeCreditPurchaseOptions): Promise<ConfirmStripeCreditPurchaseResult>;
|
|
41
|
+
openDirectPublicPaymentSession(opts: OpenDirectPublicPaymentSessionOptions): Promise<OpenPublicPaymentSessionResult>;
|
|
42
|
+
openLookupPublicPaymentSession(opts: OpenLookupPublicPaymentSessionOptions): Promise<OpenPublicPaymentSessionResult>;
|
|
43
|
+
payPublicSession(opts: PayPublicSessionOptions): Promise<PayPublicSessionResult>;
|
|
44
|
+
}
|
|
45
|
+
export declare class PaymentsFX implements PaymentsFXOperator {
|
|
46
|
+
private payments;
|
|
47
|
+
private submittedPublicSessionIDs;
|
|
48
|
+
constructor(opts: PaymentsFXOptions);
|
|
49
|
+
purchaseAppleCredits(opts: PurchaseAppleCreditsOptions): Promise<void>;
|
|
50
|
+
purchaseGoogleCredits(opts: PurchaseGoogleCreditsOptions): Promise<void>;
|
|
51
|
+
prepareStripeCreditPurchase(opts: PrepareStripeCreditPurchaseOptions): Promise<PrepareStripeCreditPurchaseResult>;
|
|
52
|
+
confirmStripeCreditPurchase(opts: ConfirmStripeCreditPurchaseOptions): Promise<ConfirmStripeCreditPurchaseResult>;
|
|
53
|
+
openDirectPublicPaymentSession(opts: OpenDirectPublicPaymentSessionOptions): Promise<OpenPublicPaymentSessionResult>;
|
|
54
|
+
openLookupPublicPaymentSession(opts: OpenLookupPublicPaymentSessionOptions): Promise<OpenPublicPaymentSessionResult>;
|
|
55
|
+
payPublicSession(opts: PayPublicSessionOptions): Promise<PayPublicSessionResult>;
|
|
56
|
+
private openPublicPaymentSession;
|
|
57
|
+
}
|
|
58
|
+
export declare function newPaymentsFX(opts: PaymentsFXOptions): PaymentsFX;
|
|
59
|
+
export {};
|