@qelos/global-types 4.1.1 → 4.2.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/index.ts CHANGED
@@ -11,8 +11,10 @@ export * from './workspace-configuration-metadata'
11
11
  export * from './auth-configuration-metadata'
12
12
  export * from './app-configuration-metadata'
13
13
  export * from './integration-sources'
14
+ export * from './integration-source-status'
14
15
  export * from './integration'
15
16
  export * from './integration-targets'
16
17
  export * from './no-code-components';
17
18
  export * from './payments';
19
+ export * from './payment-event-resolution';
18
20
  export * from './qelos-integrator';
@@ -0,0 +1,38 @@
1
+ import { IntegrationSourceKind } from './integration-sources';
2
+
3
+ export type IntegrationSourceStatusKind = 'connected' | 'failed' | 'unsupported';
4
+
5
+ /** Payload for draft status checks while creating or editing a connection. */
6
+ export interface IIntegrationSourceStatusRequest {
7
+ kind: IntegrationSourceKind;
8
+ metadata: Record<string, unknown>;
9
+ authentication?: Record<string, unknown>;
10
+ }
11
+
12
+ /** Result of a read-only integration source connection check. */
13
+ export interface IIntegrationSourceStatusResult {
14
+ status: IntegrationSourceStatusKind;
15
+ message: string;
16
+ kind: IntegrationSourceKind;
17
+ checkedAt: string;
18
+ /** Safe provider snippets (no secrets). */
19
+ details?: Record<string, unknown>;
20
+ }
21
+
22
+ export const PAYMENT_INTEGRATION_SOURCE_KINDS: IntegrationSourceKind[] = [
23
+ IntegrationSourceKind.Sumit,
24
+ IntegrationSourceKind.PayPal,
25
+ IntegrationSourceKind.Paddle,
26
+ IntegrationSourceKind.DodoPayments,
27
+ ];
28
+
29
+ /** Integration source kinds with a real, implemented connection status check. */
30
+ export const STATUS_CHECK_SUPPORTED_INTEGRATION_SOURCE_KINDS: IntegrationSourceKind[] = [
31
+ ...PAYMENT_INTEGRATION_SOURCE_KINDS,
32
+ IntegrationSourceKind.Http,
33
+ IntegrationSourceKind.OpenAI,
34
+ IntegrationSourceKind.Qelos,
35
+ IntegrationSourceKind.Email,
36
+ IntegrationSourceKind.AWS,
37
+ IntegrationSourceKind.Cloudflare,
38
+ ];
@@ -88,6 +88,7 @@ export const SumitTargetOperation = {
88
88
  updatePayment: 'updatePayment',
89
89
  // Placeholder for recurring payments
90
90
  listRecurringPayments: 'listRecurringPayments',
91
+ beginCheckoutRedirect: 'beginCheckoutRedirect',
91
92
  createRecurringPayment: 'createRecurringPayment',
92
93
  getRecurringPayment: 'getRecurringPayment',
93
94
  updateRecurringPayment: 'updateRecurringPayment',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/global-types",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "",
5
5
  "main": "index.ts",
6
6
  "types": "index.ts",
@@ -0,0 +1,388 @@
1
+ export type PaymentAdminSuggestion = {
2
+ summary: string;
3
+ action: string;
4
+ };
5
+
6
+ export type PaymentProviderPublicContext = {
7
+ providerSourceId?: string;
8
+ providerPublicAccountId?: string;
9
+ providerEnvironment?: string;
10
+ };
11
+
12
+ export type PaymentResolutionContext = {
13
+ providerKind?: string;
14
+ operation?: string;
15
+ code?: string;
16
+ status?: number;
17
+ message?: string;
18
+ providerError?: Record<string, unknown> | null;
19
+ };
20
+
21
+ export type PaymentEventDocsContext = {
22
+ providerKind?: string;
23
+ eventName?: string;
24
+ operation?: string;
25
+ code?: string;
26
+ };
27
+
28
+ export const PAYMENTS_DOCS_BASE_URL = 'https://docs.qelos.io/payments';
29
+
30
+ const TROUBLESHOOTING_ERROR_CODES = new Set([
31
+ 'PAYMENTS_NOT_CONFIGURED',
32
+ 'ACTIVE_SUBSCRIPTION_EXISTS',
33
+ 'DYNAMIC_AMOUNT_NOT_SET',
34
+ 'DYNAMIC_PLAN_REQUIRES_SUBSCRIPTION',
35
+ 'DYNAMIC_PLAN_UNSUPPORTED_PROVIDER',
36
+ 'MISSING_REDIRECT_URLS',
37
+ 'INVALID_CHECKOUT_AMOUNT',
38
+ 'INVALID_SUMIT_COMPANY_ID',
39
+ 'MISSING_SUMIT_CREDENTIALS',
40
+ 'UNSUPPORTED_SUMIT_CURRENCY',
41
+ 'INTEGRATION_SOURCE_NOT_FOUND',
42
+ 'MISSING_EXTERNAL_PRICE_ID',
43
+ 'PLAN_NOT_ACTIVE',
44
+ 'SUBSCRIPTION_NOT_PENDING',
45
+ 'INVALID_SIGNATURE',
46
+ 'WEBHOOK_SECRET_NOT_CONFIGURED',
47
+ 'TENANT_NOT_FOUND',
48
+ ]);
49
+
50
+ function troubleshootingCodeAnchor(code: string) {
51
+ return code.toLowerCase().replace(/_/g, '-');
52
+ }
53
+
54
+ function resolveProviderEventDocsUrl(providerKind: string, eventName?: string) {
55
+ const provider = providerKind.toLowerCase();
56
+
57
+ if (eventName === 'payment-method-save-failed') {
58
+ return `${PAYMENTS_DOCS_BASE_URL}/events#failed-credit-card-capture-payment-method-save-failed`;
59
+ }
60
+
61
+ if (eventName === 'payment-failed' || eventName === 'webhook-processing-failed') {
62
+ return `${PAYMENTS_DOCS_BASE_URL}/events#${provider}-troubleshooting`;
63
+ }
64
+
65
+ if (eventName === 'checkout-failed' || eventName === 'provider-call-failed') {
66
+ switch (provider) {
67
+ case 'sumit':
68
+ return `${PAYMENTS_DOCS_BASE_URL}/events#checkout-initiation-failure-checkout-failed-or-provider-call-failed`;
69
+ case 'paddle':
70
+ return `${PAYMENTS_DOCS_BASE_URL}/events#checkout-validation-failure-checkout-failed`;
71
+ case 'paypal':
72
+ return `${PAYMENTS_DOCS_BASE_URL}/events#checkout-and-subscription-setup-failure-checkout-failed-or-provider-call-failed`;
73
+ case 'dodopayments':
74
+ return `${PAYMENTS_DOCS_BASE_URL}/events#checkout-failure-checkout-failed-or-provider-call-failed`;
75
+ default:
76
+ break;
77
+ }
78
+ }
79
+
80
+ switch (provider) {
81
+ case 'sumit':
82
+ return `${PAYMENTS_DOCS_BASE_URL}/events#sumit-troubleshooting`;
83
+ case 'paddle':
84
+ return `${PAYMENTS_DOCS_BASE_URL}/events#paddle-troubleshooting`;
85
+ case 'paypal':
86
+ return `${PAYMENTS_DOCS_BASE_URL}/events#paypal-troubleshooting`;
87
+ case 'dodopayments':
88
+ return `${PAYMENTS_DOCS_BASE_URL}/events#dodopayments-troubleshooting`;
89
+ default:
90
+ return `${PAYMENTS_DOCS_BASE_URL}/troubleshooting`;
91
+ }
92
+ }
93
+
94
+ export function resolvePaymentEventDocsUrl(context: PaymentEventDocsContext): string {
95
+ const code = context.code?.toUpperCase();
96
+
97
+ if (code && TROUBLESHOOTING_ERROR_CODES.has(code)) {
98
+ return `${PAYMENTS_DOCS_BASE_URL}/troubleshooting#${troubleshootingCodeAnchor(code)}`;
99
+ }
100
+
101
+ if (context.providerKind) {
102
+ return resolveProviderEventDocsUrl(context.providerKind, context.eventName);
103
+ }
104
+
105
+ return `${PAYMENTS_DOCS_BASE_URL}/troubleshooting`;
106
+ }
107
+
108
+ function pushUnique(suggestions: PaymentAdminSuggestion[], suggestion: PaymentAdminSuggestion) {
109
+ if (suggestions.some((s) => s.summary === suggestion.summary)) {
110
+ return;
111
+ }
112
+ suggestions.push(suggestion);
113
+ }
114
+
115
+ function sumitCredentialsSuggestions(
116
+ suggestions: PaymentAdminSuggestion[],
117
+ message?: string,
118
+ providerError?: Record<string, unknown> | null,
119
+ ) {
120
+ const combined = [
121
+ message,
122
+ providerError?.UserErrorMessage,
123
+ providerError?.TechnicalErrorDetails,
124
+ ]
125
+ .filter(Boolean)
126
+ .join(' ')
127
+ .toLowerCase();
128
+
129
+ if (
130
+ combined.includes('credential')
131
+ || combined.includes('apikey')
132
+ || combined.includes('api key')
133
+ || combined.includes('companyid')
134
+ || combined.includes('company id')
135
+ || combined.includes('unauthorized')
136
+ || combined.includes('authentication')
137
+ ) {
138
+ pushUnique(suggestions, {
139
+ summary: 'Verify Sumit API credentials',
140
+ action: 'Open Admin → Integrations → Sumit, confirm Company ID matches https://app.sumit.co.il/developers/keys/ and re-save a freshly generated API key.',
141
+ });
142
+ }
143
+ }
144
+
145
+ export function sanitizeProviderErrorBody(value: unknown): Record<string, unknown> | null {
146
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
147
+ return null;
148
+ }
149
+
150
+ const sanitized: Record<string, unknown> = {};
151
+ for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
152
+ const normalized = key.replace(/[_-]/g, '').toLowerCase();
153
+ if (
154
+ normalized === 'credentials'
155
+ || normalized === 'apikey'
156
+ || normalized === 'companyid'
157
+ || normalized === 'singleusetoken'
158
+ || normalized === 'cardnumber'
159
+ || normalized === 'creditcardnumber'
160
+ || normalized === 'cvv'
161
+ || normalized === 'cvc'
162
+ || normalized === 'password'
163
+ || normalized === 'secret'
164
+ ) {
165
+ continue;
166
+ }
167
+ sanitized[key] = nestedValue;
168
+ }
169
+ return sanitized;
170
+ }
171
+
172
+ export function extractSumitProviderError(responseBody: unknown): Record<string, unknown> | null {
173
+ const sanitized = sanitizeProviderErrorBody(responseBody);
174
+ if (!sanitized) {
175
+ return null;
176
+ }
177
+
178
+ if (sanitized.providerError && typeof sanitized.providerError === 'object') {
179
+ return sanitizeProviderErrorBody(sanitized.providerError);
180
+ }
181
+
182
+ if (
183
+ sanitized.UserErrorMessage
184
+ || sanitized.TechnicalErrorDetails
185
+ || sanitized.Status
186
+ ) {
187
+ return sanitized;
188
+ }
189
+
190
+ if (sanitized.Data && typeof sanitized.Data === 'object') {
191
+ return sanitizeProviderErrorBody(sanitized.Data);
192
+ }
193
+
194
+ return sanitized;
195
+ }
196
+
197
+ export function resolvePaymentProviderPublicContext(
198
+ providerKind: string | undefined,
199
+ metadata?: Record<string, unknown> | null,
200
+ providerSourceId?: string,
201
+ ): PaymentProviderPublicContext {
202
+ const context: PaymentProviderPublicContext = {};
203
+
204
+ if (providerSourceId) {
205
+ context.providerSourceId = providerSourceId;
206
+ }
207
+
208
+ if (!providerKind || !metadata) {
209
+ return context;
210
+ }
211
+
212
+ switch (providerKind) {
213
+ case 'sumit':
214
+ if (metadata.companyId != null && metadata.companyId !== '') {
215
+ context.providerPublicAccountId = String(metadata.companyId);
216
+ }
217
+ break;
218
+ case 'paypal':
219
+ if (typeof metadata.clientId === 'string' && metadata.clientId) {
220
+ context.providerPublicAccountId = metadata.clientId;
221
+ }
222
+ if (typeof metadata.environment === 'string' && metadata.environment) {
223
+ context.providerEnvironment = metadata.environment;
224
+ }
225
+ break;
226
+ case 'paddle':
227
+ if (typeof metadata.environment === 'string' && metadata.environment) {
228
+ context.providerEnvironment = metadata.environment;
229
+ }
230
+ break;
231
+ case 'dodopayments':
232
+ if (typeof metadata.environment === 'string' && metadata.environment) {
233
+ context.providerEnvironment = metadata.environment;
234
+ }
235
+ break;
236
+ default:
237
+ break;
238
+ }
239
+
240
+ return context;
241
+ }
242
+
243
+ export function appendPaymentProviderContext<T extends Record<string, unknown>>(
244
+ metadata: T,
245
+ providerContext?: PaymentProviderPublicContext,
246
+ ): T & Partial<PaymentProviderPublicContext> {
247
+ if (!providerContext) {
248
+ return metadata;
249
+ }
250
+
251
+ return {
252
+ ...metadata,
253
+ ...(providerContext.providerSourceId ? { providerSourceId: providerContext.providerSourceId } : {}),
254
+ ...(providerContext.providerPublicAccountId ? { providerPublicAccountId: providerContext.providerPublicAccountId } : {}),
255
+ ...(providerContext.providerEnvironment ? { providerEnvironment: providerContext.providerEnvironment } : {}),
256
+ };
257
+ }
258
+
259
+ export function buildPaymentAdminSuggestions(context: PaymentResolutionContext): PaymentAdminSuggestion[] {
260
+ const suggestions: PaymentAdminSuggestion[] = [];
261
+ const code = context.code?.toUpperCase();
262
+ const message = context.message?.toLowerCase() || '';
263
+ const providerError = context.providerError;
264
+
265
+ if (code === 'PAYMENTS_NOT_CONFIGURED') {
266
+ pushUnique(suggestions, {
267
+ summary: 'Configure payments for this tenant',
268
+ action: 'Open Admin → Pricing Plans → Configuration, enable payments, and select a payment provider integration source.',
269
+ });
270
+ }
271
+
272
+ if (code === 'ACTIVE_SUBSCRIPTION_EXISTS') {
273
+ pushUnique(suggestions, {
274
+ summary: 'Cancel the existing subscription before checkout',
275
+ action: 'The billable entity already has an active or trialing subscription. Cancel it with sdk.payments.cancelSubscription(existingSubscriptionId), or pass reset: true on checkout to cancel automatically and start a new subscription.',
276
+ });
277
+ }
278
+
279
+ if (code === 'DYNAMIC_AMOUNT_NOT_SET') {
280
+ pushUnique(suggestions, {
281
+ summary: 'Set the subscription amount before checkout',
282
+ action: 'This plan is dynamic-priced. An admin must set dynamicAmount on the pending subscription before the user can pay.',
283
+ });
284
+ }
285
+
286
+ if (code === 'DYNAMIC_PLAN_REQUIRES_SUBSCRIPTION' || code === 'DYNAMIC_PLAN_UNSUPPORTED_PROVIDER') {
287
+ pushUnique(suggestions, {
288
+ summary: 'Use the dynamic-plan checkout flow',
289
+ action: 'Create a pending subscription first, set dynamicAmount (admin), then call checkout with subscriptionId. Dynamic plans require Sumit.',
290
+ });
291
+ }
292
+
293
+ if (code === 'MISSING_REDIRECT_URLS') {
294
+ pushUnique(suggestions, {
295
+ summary: 'Configure checkout redirect URLs',
296
+ action: 'Add successUrl and cancelUrl in Admin → Pricing Plans → Configuration, or pass them in the checkout request.',
297
+ });
298
+ }
299
+
300
+ if (code === 'INVALID_CHECKOUT_AMOUNT') {
301
+ pushUnique(suggestions, {
302
+ summary: 'Set a positive checkout amount',
303
+ action: 'For dynamic plans, an admin must set dynamicAmount on the pending subscription before checkout. Static plans need a non-zero monthly/yearly price.',
304
+ });
305
+ }
306
+
307
+ if (code === 'INVALID_SUMIT_COMPANY_ID' || code === 'MISSING_SUMIT_CREDENTIALS') {
308
+ pushUnique(suggestions, {
309
+ summary: 'Fix Sumit Company ID',
310
+ action: 'Open Admin → Integrations → Sumit and enter the numeric Company ID from https://app.sumit.co.il/developers/keys/.',
311
+ });
312
+ }
313
+
314
+ if (code === 'UNSUPPORTED_SUMIT_CURRENCY') {
315
+ pushUnique(suggestions, {
316
+ summary: 'Use a Sumit-supported plan currency',
317
+ action: 'Change the plan currency to ILS, USD, or EUR. Sumit does not support other currencies.',
318
+ });
319
+ }
320
+
321
+ if (code === 'INTEGRATION_SOURCE_NOT_FOUND') {
322
+ pushUnique(suggestions, {
323
+ summary: 'Reconnect the payment provider',
324
+ action: 'The configured paymentSourceId no longer exists. Re-select the Sumit integration in Admin → Pricing Plans → Configuration.',
325
+ });
326
+ }
327
+
328
+ if (context.providerKind === 'sumit') {
329
+ sumitCredentialsSuggestions(suggestions, context.message, providerError);
330
+
331
+ if (context.operation === 'beginCheckoutRedirect') {
332
+ pushUnique(suggestions, {
333
+ summary: 'Confirm Sumit hosted checkout is enabled',
334
+ action: 'In the Sumit account, verify redirect/hosted checkout is enabled for the merchant and that the API key has billing permissions.',
335
+ });
336
+ }
337
+
338
+ if (message.includes('unsupported sumit currency')) {
339
+ pushUnique(suggestions, {
340
+ summary: 'Use a Sumit-supported plan currency',
341
+ action: 'Set the plan currency to ILS, USD, or EUR before retrying checkout.',
342
+ });
343
+ }
344
+
345
+ if (message.includes('missing api key or company id')) {
346
+ pushUnique(suggestions, {
347
+ summary: 'Complete the Sumit integration credentials',
348
+ action: 'Open Admin → Integrations → Sumit and save both Company ID and API key.',
349
+ });
350
+ }
351
+ }
352
+
353
+ if (
354
+ context.status === 401
355
+ || context.status === 403
356
+ || message.includes('401')
357
+ || message.includes('403')
358
+ ) {
359
+ pushUnique(suggestions, {
360
+ summary: 'Payment provider rejected the credentials',
361
+ action: 'Regenerate the provider API key/secret in the provider dashboard and update the integration source in Admin → Integrations.',
362
+ });
363
+ }
364
+
365
+ if (
366
+ code === 'INTEGRATION_TARGET_FAILED'
367
+ && suggestions.length === 0
368
+ && context.providerKind === 'sumit'
369
+ ) {
370
+ pushUnique(suggestions, {
371
+ summary: 'Review Sumit account configuration',
372
+ action: 'Verify Company ID and API key in Admin → Integrations → Sumit, then retry checkout. If it still fails, check Sumit developer logs for the rejected beginredirect request.',
373
+ });
374
+ }
375
+
376
+ return suggestions;
377
+ }
378
+
379
+ export function buildPaymentEventDescription(
380
+ baseDescription: string,
381
+ providerError?: Record<string, unknown> | null,
382
+ ): string {
383
+ const userMessage = providerError?.UserErrorMessage;
384
+ if (typeof userMessage === 'string' && userMessage.trim()) {
385
+ return `${baseDescription}: ${userMessage.trim()}`;
386
+ }
387
+ return baseDescription;
388
+ }
package/payments.ts CHANGED
@@ -13,6 +13,23 @@ export type InvoiceStatus = 'paid' | 'pending' | 'failed' | 'refunded';
13
13
  /** Type of discount a coupon provides. */
14
14
  export type CouponDiscountType = 'percentage' | 'fixed';
15
15
 
16
+ /** Tenant payments settings stored in the `payments-configuration` configuration key. */
17
+ export interface IPaymentsConfigurationMetadata {
18
+ isEnabled?: boolean;
19
+ /** Integration source `_id` (admin UI field name). */
20
+ paymentSourceId?: string;
21
+ /** Same as `paymentSourceId`; normalized by the payments service when loading config. */
22
+ providerSourceId?: string;
23
+ providerKind?: 'paddle' | 'paypal' | 'sumit' | 'dodopayments' | string;
24
+ defaultCurrency?: string;
25
+ gracePeriodDays?: number;
26
+ /** Default redirect URL after successful checkout (required for Sumit unless overridden per checkout). */
27
+ successUrl?: string;
28
+ /** Default redirect URL when the user cancels checkout (required for Sumit unless overridden per checkout). */
29
+ cancelUrl?: string;
30
+ webhookSecret?: string;
31
+ }
32
+
16
33
  /**
17
34
  * Arbitrary limits associated with a plan (e.g. max users, storage quota).
18
35
  * Keys are limit names; values can be numbers, booleans, or strings.