@progus/connector 0.6.7 → 0.7.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/README.md +91 -47
- package/dist/{crossSell-Bs_6xA6f.d.cts → crossSell-BdqPj2g7.d.cts} +3 -2
- package/dist/{crossSell-Bs_6xA6f.d.ts → crossSell-BdqPj2g7.d.ts} +3 -2
- package/dist/crossSell.d.cts +1 -1
- package/dist/crossSell.d.ts +1 -1
- package/dist/index.cjs +156 -141
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +156 -141
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,80 +8,124 @@ Headless connector for Progus partner/affiliate tracking and partner ID assignme
|
|
|
8
8
|
npm install @progus/connector
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Minimal Integration
|
|
12
|
+
|
|
13
|
+
Integrate the partner program in 4 steps:
|
|
14
|
+
|
|
15
|
+
### 1. Server connector setup
|
|
12
16
|
|
|
13
17
|
```ts
|
|
18
|
+
// utils/progus-connector.server.ts
|
|
14
19
|
import { createProgusConnector } from "@progus/connector";
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
export function isPartnersConnectorConfigured(): boolean {
|
|
22
|
+
return Boolean(process.env.PARTNERS_API_URL && process.env.PARTNERS_SECRET_KEY);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getPartnersConnector() {
|
|
26
|
+
return createProgusConnector({
|
|
27
|
+
appKey: "your-app-name",
|
|
28
|
+
apiBaseUrl: process.env.PARTNERS_API_URL || "",
|
|
29
|
+
signingSecret: process.env.PARTNERS_SECRET_KEY || "",
|
|
30
|
+
apiKey: process.env.PARTNERS_API_KEY,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Cookie-based install tracking (client-side)
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
```ts
|
|
38
|
+
// On app load (e.g. in root route useEffect)
|
|
39
|
+
const cookieMatch = document.cookie.match(/(?:^|; )progus_partner_id=([^;]*)/);
|
|
40
|
+
const partnerId = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
|
|
41
|
+
|
|
42
|
+
if (partnerId) {
|
|
43
|
+
await connector.trackInstall({ shopDomain: session.shop, partnerId });
|
|
44
|
+
document.cookie = "progus_partner_id=; Path=/; Max-Age=0"; // clear after tracking
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Partner ID assignment (Save Partner ID button)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// On "Save Partner ID" action
|
|
52
|
+
const result = await connector.assignPartnerIdWithSubscription({
|
|
24
53
|
shopDomain: session.shop,
|
|
25
|
-
partnerId:
|
|
54
|
+
partnerId: inputValue,
|
|
55
|
+
admin, // Shopify Admin GraphQL client
|
|
26
56
|
});
|
|
27
57
|
|
|
58
|
+
if (result.success) {
|
|
59
|
+
// Lock the input field, show success
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Required webhooks
|
|
64
|
+
|
|
65
|
+
**APP_SUBSCRIPTIONS_UPDATE** - track subscription changes:
|
|
66
|
+
```ts
|
|
28
67
|
await connector.trackSubscriptionUpdated({
|
|
29
|
-
shopDomain:
|
|
68
|
+
shopDomain: shop,
|
|
30
69
|
data: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
subscriptionId, // numeric ID (required)
|
|
71
|
+
subscriptionPrice, // number (required)
|
|
72
|
+
subscriptionPeriod, // "EVERY_30_DAYS" or "ANNUAL" (required)
|
|
73
|
+
subscriptionStatus, // e.g. "ACTIVE", "CANCELLED"
|
|
74
|
+
subscriptionName, // plan name
|
|
34
75
|
},
|
|
35
76
|
});
|
|
77
|
+
```
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
**APP_UNINSTALLED** - track uninstalls:
|
|
80
|
+
```ts
|
|
81
|
+
await connector.trackUninstall({ shopDomain: shop });
|
|
38
82
|
```
|
|
39
83
|
|
|
40
|
-
## Prompt: Partner program implementation
|
|
84
|
+
## AI Prompt: Partner program implementation
|
|
85
|
+
|
|
86
|
+
Copy/paste for AI assistants:
|
|
41
87
|
|
|
42
88
|
```
|
|
43
89
|
You are integrating Progus Partner Program using @progus/connector in a Shopify app.
|
|
44
90
|
|
|
45
|
-
|
|
46
|
-
1) Server connector:
|
|
47
|
-
- createProgusConnector({ appKey, PARTNERS_API_URL, PARTNERS_SECRET_KEY, PARTNERS_API_KEY })
|
|
48
|
-
- expose getPartnersConnector() + isPartnersConnectorConfigured()
|
|
91
|
+
Required files to create/modify:
|
|
49
92
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- cookie must be JS-readable (HttpOnly=false) and cross-site (SameSite=None; Secure)
|
|
54
|
-
- if partnerId is missing, skip tracking
|
|
93
|
+
1) Server connector (utils/progus-connector.server.ts):
|
|
94
|
+
- createProgusConnector({ appKey, apiBaseUrl, signingSecret, apiKey })
|
|
95
|
+
- export getPartnersConnector() and isPartnersConnectorConfigured()
|
|
55
96
|
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
- call
|
|
59
|
-
-
|
|
60
|
-
-
|
|
97
|
+
2) Cookie-based install tracking (client-side, on app load):
|
|
98
|
+
- read cookie "progus_partner_id" (HttpOnly=false, SameSite=None, Secure)
|
|
99
|
+
- if exists: call connector.trackInstall({ shopDomain, partnerId })
|
|
100
|
+
- clear cookie after successful tracking
|
|
101
|
+
- if no cookie, skip tracking
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
103
|
+
3) Partner ID assignment UI (on "Save Partner ID" button):
|
|
104
|
+
- call connector.assignPartnerIdWithSubscription({ shopDomain, partnerId, admin })
|
|
105
|
+
- admin = Shopify Admin GraphQL client
|
|
106
|
+
- connector normalizes partnerId and fetches active subscription automatically
|
|
107
|
+
- subscription event sent ONLY if price + period are available
|
|
108
|
+
- lock the Partner ID field after success
|
|
109
|
+
- optionally: check existing partnerId on load with connector.checkPartnerId({ shopDomain })
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
-
|
|
111
|
+
4) APP_SUBSCRIPTIONS_UPDATE webhook (required):
|
|
112
|
+
- call connector.trackSubscriptionUpdated({ shopDomain, data })
|
|
113
|
+
- data must include: subscriptionId (number), subscriptionPrice (number), subscriptionPeriod ("EVERY_30_DAYS" | "ANNUAL")
|
|
114
|
+
- if webhook payload missing price/interval, query: node(id) -> AppSubscription -> lineItems.plan.pricingDetails
|
|
115
|
+
- connector validates; event skipped if price/period missing
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
5) APP_UNINSTALLED webhook (required):
|
|
118
|
+
- call connector.trackUninstall({ shopDomain })
|
|
72
119
|
|
|
73
|
-
|
|
120
|
+
Error handling: catch and log errors; never break webhooks on partner tracking failure.
|
|
121
|
+
```
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
const result = await connector.assignPartnerId({
|
|
77
|
-
shopDomain: "my-shop.myshopify.com",
|
|
78
|
-
partnerId: "partner_123",
|
|
79
|
-
});
|
|
123
|
+
## Validation
|
|
80
124
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
125
|
+
The connector handles all validation internally:
|
|
126
|
+
- `trackInstall` normalizes partnerId (trims, checks format)
|
|
127
|
+
- `trackSubscriptionUpdated` validates price/period; skips if missing
|
|
128
|
+
- Apps do not need to validate before calling connector methods
|
|
85
129
|
|
|
86
130
|
## Optional signing helper
|
|
87
131
|
|
|
@@ -40,9 +40,10 @@ type CheckPartnerIdResult = {
|
|
|
40
40
|
message?: string;
|
|
41
41
|
status?: number;
|
|
42
42
|
};
|
|
43
|
-
type
|
|
43
|
+
type AssignPartnerIdWithSubscriptionInput = {
|
|
44
44
|
shopDomain: string;
|
|
45
45
|
partnerId: string;
|
|
46
|
+
admin: ShopifyAdminGraphQLClient;
|
|
46
47
|
};
|
|
47
48
|
type SubscriptionEventData = {
|
|
48
49
|
subscriptionStatus?: string;
|
|
@@ -82,4 +83,4 @@ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntr
|
|
|
82
83
|
declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
|
|
83
84
|
declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
|
|
84
85
|
|
|
85
|
-
export { type
|
|
86
|
+
export { type AssignPartnerIdWithSubscriptionInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
|
|
@@ -40,9 +40,10 @@ type CheckPartnerIdResult = {
|
|
|
40
40
|
message?: string;
|
|
41
41
|
status?: number;
|
|
42
42
|
};
|
|
43
|
-
type
|
|
43
|
+
type AssignPartnerIdWithSubscriptionInput = {
|
|
44
44
|
shopDomain: string;
|
|
45
45
|
partnerId: string;
|
|
46
|
+
admin: ShopifyAdminGraphQLClient;
|
|
46
47
|
};
|
|
47
48
|
type SubscriptionEventData = {
|
|
48
49
|
subscriptionStatus?: string;
|
|
@@ -82,4 +83,4 @@ declare function getCrossSellOffers(options?: CrossSellOptions): AppsCatalogEntr
|
|
|
82
83
|
declare function fetchAppsCatalog(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
|
|
83
84
|
declare function getCrossSellOffersFromApi(options?: CrossSellFetchOptions): Promise<AppsCatalogEntry[]>;
|
|
84
85
|
|
|
85
|
-
export { type
|
|
86
|
+
export { type AssignPartnerIdWithSubscriptionInput as A, type ConnectorConfig as C, type Logger as L, type SubscriptionEventData as S, type TrackEventParams as T, type TrackResult as a, type CheckPartnerIdResult as b, type ShopifyAdminGraphQLClient as c, getCrossSellOffersFromApi as d, type AppsCatalogEntry as e, fetchAppsCatalog as f, getCrossSellOffers as g, type CrossSellFetchOptions as h, type CrossSellOptions as i, type TrackEventName as j };
|
package/dist/crossSell.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-
|
|
1
|
+
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.cjs';
|
package/dist/crossSell.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-
|
|
1
|
+
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.js';
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,142 @@ __export(index_exports, {
|
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
32
|
|
|
33
|
+
// src/subscription.ts
|
|
34
|
+
var ACTIVE_SUBSCRIPTIONS_QUERY = `
|
|
35
|
+
query {
|
|
36
|
+
currentAppInstallation {
|
|
37
|
+
activeSubscriptions {
|
|
38
|
+
id
|
|
39
|
+
name
|
|
40
|
+
status
|
|
41
|
+
lineItems {
|
|
42
|
+
plan {
|
|
43
|
+
pricingDetails {
|
|
44
|
+
__typename
|
|
45
|
+
... on AppRecurringPricing {
|
|
46
|
+
interval
|
|
47
|
+
price {
|
|
48
|
+
amount
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
... on AppUsagePricing {
|
|
52
|
+
cappedAmount {
|
|
53
|
+
amount
|
|
54
|
+
}
|
|
55
|
+
terms
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
var SUBSCRIPTION_DETAILS_QUERY = `
|
|
65
|
+
query AppSubscriptionDetails($id: ID!) {
|
|
66
|
+
node(id: $id) {
|
|
67
|
+
... on AppSubscription {
|
|
68
|
+
id
|
|
69
|
+
name
|
|
70
|
+
status
|
|
71
|
+
lineItems {
|
|
72
|
+
plan {
|
|
73
|
+
pricingDetails {
|
|
74
|
+
__typename
|
|
75
|
+
... on AppRecurringPricing {
|
|
76
|
+
interval
|
|
77
|
+
price {
|
|
78
|
+
amount
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
... on AppUsagePricing {
|
|
82
|
+
cappedAmount {
|
|
83
|
+
amount
|
|
84
|
+
}
|
|
85
|
+
terms
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
function normalizeInterval(intervalRaw, nameHint) {
|
|
95
|
+
const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
|
|
96
|
+
if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
|
|
97
|
+
if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
|
|
98
|
+
const name = (nameHint ?? "").toLowerCase();
|
|
99
|
+
if (name.includes("annual") || name.includes("year")) return "ANNUAL";
|
|
100
|
+
if (name) return "EVERY_30_DAYS";
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
function parseSubscriptionId(rawId) {
|
|
104
|
+
if (!rawId) return null;
|
|
105
|
+
return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
|
|
106
|
+
}
|
|
107
|
+
function parsePrice(rawPrice) {
|
|
108
|
+
if (typeof rawPrice === "number") return rawPrice;
|
|
109
|
+
if (typeof rawPrice === "string") {
|
|
110
|
+
const parsed = Number(rawPrice);
|
|
111
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function extractPricing(subscription) {
|
|
116
|
+
const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
|
|
117
|
+
const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
|
|
118
|
+
const subscriptionPrice = parsePrice(rawPrice);
|
|
119
|
+
const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
|
|
120
|
+
return { subscriptionPrice, subscriptionPeriod };
|
|
121
|
+
}
|
|
122
|
+
async function fetchActiveSubscriptionEventData(admin, logger = console) {
|
|
123
|
+
const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
|
|
124
|
+
const result = await response.json();
|
|
125
|
+
if (result.errors?.length) {
|
|
126
|
+
logger.error?.("Partner subscription query errors", { errors: result.errors });
|
|
127
|
+
}
|
|
128
|
+
const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
|
|
129
|
+
const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
|
|
130
|
+
const subscriptionId = parseSubscriptionId(active?.id);
|
|
131
|
+
if (!subscriptionId) return null;
|
|
132
|
+
let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
|
|
133
|
+
if (!subscriptionPeriod || subscriptionPrice === null) {
|
|
134
|
+
try {
|
|
135
|
+
const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
|
|
136
|
+
variables: { id: active?.id }
|
|
137
|
+
});
|
|
138
|
+
const detailsJson = await detailsResponse.json();
|
|
139
|
+
if (detailsJson.errors?.length) {
|
|
140
|
+
logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
|
|
141
|
+
}
|
|
142
|
+
const details = detailsJson.data?.node;
|
|
143
|
+
({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
|
|
144
|
+
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
145
|
+
return {
|
|
146
|
+
subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
|
|
147
|
+
subscriptionName: details?.name ?? active?.name,
|
|
148
|
+
subscriptionId,
|
|
149
|
+
subscriptionPrice,
|
|
150
|
+
subscriptionPeriod
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error?.("Partner subscription details query failed", {
|
|
154
|
+
error: error instanceof Error ? error.message : String(error)
|
|
155
|
+
});
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
160
|
+
return {
|
|
161
|
+
subscriptionStatus: active?.status ?? "ACTIVE",
|
|
162
|
+
subscriptionName: active?.name,
|
|
163
|
+
subscriptionId,
|
|
164
|
+
subscriptionPrice,
|
|
165
|
+
subscriptionPeriod
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
33
169
|
// src/signing.ts
|
|
34
170
|
var import_crypto = require("crypto");
|
|
35
171
|
function signPayload(body, secret) {
|
|
@@ -75,7 +211,7 @@ function createProgusConnector(config) {
|
|
|
75
211
|
trackSubscriptionPurchased: fail,
|
|
76
212
|
trackSubscriptionUpdated: fail,
|
|
77
213
|
trackSubscriptionCancelled: fail,
|
|
78
|
-
|
|
214
|
+
assignPartnerIdWithSubscription: fail,
|
|
79
215
|
checkPartnerId: async () => ({ success: false, message, status: 500 })
|
|
80
216
|
};
|
|
81
217
|
}
|
|
@@ -169,12 +305,27 @@ function createProgusConnector(config) {
|
|
|
169
305
|
externalId: String(normalized.subscriptionId)
|
|
170
306
|
});
|
|
171
307
|
}
|
|
172
|
-
async function
|
|
173
|
-
const
|
|
308
|
+
async function assignPartnerIdWithSubscription(payload) {
|
|
309
|
+
const { admin, shopDomain, partnerId } = payload;
|
|
310
|
+
const normalized = normalizePartnerId(partnerId);
|
|
174
311
|
if (!normalized) {
|
|
175
312
|
return { success: false, message: "Partner ID is required" };
|
|
176
313
|
}
|
|
177
|
-
const result = await trackInstall({ shopDomain
|
|
314
|
+
const result = await trackInstall({ shopDomain, partnerId: normalized });
|
|
315
|
+
if (!result.success) return { ...result, partnerId: normalized };
|
|
316
|
+
try {
|
|
317
|
+
const subscriptionPayload = await fetchActiveSubscriptionEventData(admin, logger);
|
|
318
|
+
if (subscriptionPayload) {
|
|
319
|
+
await trackSubscription({
|
|
320
|
+
shopDomain,
|
|
321
|
+
data: subscriptionPayload
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
logger?.error?.("Partner subscription sync failed", {
|
|
326
|
+
error: error instanceof Error ? error.message : String(error)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
178
329
|
return { ...result, partnerId: normalized };
|
|
179
330
|
}
|
|
180
331
|
async function checkPartnerId(payload) {
|
|
@@ -246,7 +397,7 @@ function createProgusConnector(config) {
|
|
|
246
397
|
trackSubscriptionPurchased: trackSubscription,
|
|
247
398
|
trackSubscriptionUpdated: trackSubscription,
|
|
248
399
|
trackSubscriptionCancelled: trackSubscription,
|
|
249
|
-
|
|
400
|
+
assignPartnerIdWithSubscription,
|
|
250
401
|
checkPartnerId
|
|
251
402
|
};
|
|
252
403
|
}
|
|
@@ -349,142 +500,6 @@ async function getCrossSellOffersFromApi(options = {}) {
|
|
|
349
500
|
const appsCatalog = options.appsCatalog ?? await fetchAppsCatalog(options);
|
|
350
501
|
return getCrossSellOffers({ ...options, appsCatalog });
|
|
351
502
|
}
|
|
352
|
-
|
|
353
|
-
// src/subscription.ts
|
|
354
|
-
var ACTIVE_SUBSCRIPTIONS_QUERY = `
|
|
355
|
-
query {
|
|
356
|
-
currentAppInstallation {
|
|
357
|
-
activeSubscriptions {
|
|
358
|
-
id
|
|
359
|
-
name
|
|
360
|
-
status
|
|
361
|
-
lineItems {
|
|
362
|
-
plan {
|
|
363
|
-
pricingDetails {
|
|
364
|
-
__typename
|
|
365
|
-
... on AppRecurringPricing {
|
|
366
|
-
interval
|
|
367
|
-
price {
|
|
368
|
-
amount
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
... on AppUsagePricing {
|
|
372
|
-
cappedAmount {
|
|
373
|
-
amount
|
|
374
|
-
}
|
|
375
|
-
terms
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
`;
|
|
384
|
-
var SUBSCRIPTION_DETAILS_QUERY = `
|
|
385
|
-
query AppSubscriptionDetails($id: ID!) {
|
|
386
|
-
node(id: $id) {
|
|
387
|
-
... on AppSubscription {
|
|
388
|
-
id
|
|
389
|
-
name
|
|
390
|
-
status
|
|
391
|
-
lineItems {
|
|
392
|
-
plan {
|
|
393
|
-
pricingDetails {
|
|
394
|
-
__typename
|
|
395
|
-
... on AppRecurringPricing {
|
|
396
|
-
interval
|
|
397
|
-
price {
|
|
398
|
-
amount
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
... on AppUsagePricing {
|
|
402
|
-
cappedAmount {
|
|
403
|
-
amount
|
|
404
|
-
}
|
|
405
|
-
terms
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
`;
|
|
414
|
-
function normalizeInterval(intervalRaw, nameHint) {
|
|
415
|
-
const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
|
|
416
|
-
if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
|
|
417
|
-
if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
|
|
418
|
-
const name = (nameHint ?? "").toLowerCase();
|
|
419
|
-
if (name.includes("annual") || name.includes("year")) return "ANNUAL";
|
|
420
|
-
if (name) return "EVERY_30_DAYS";
|
|
421
|
-
return "";
|
|
422
|
-
}
|
|
423
|
-
function parseSubscriptionId(rawId) {
|
|
424
|
-
if (!rawId) return null;
|
|
425
|
-
return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
|
|
426
|
-
}
|
|
427
|
-
function parsePrice(rawPrice) {
|
|
428
|
-
if (typeof rawPrice === "number") return rawPrice;
|
|
429
|
-
if (typeof rawPrice === "string") {
|
|
430
|
-
const parsed = Number(rawPrice);
|
|
431
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
432
|
-
}
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
function extractPricing(subscription) {
|
|
436
|
-
const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
|
|
437
|
-
const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
|
|
438
|
-
const subscriptionPrice = parsePrice(rawPrice);
|
|
439
|
-
const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
|
|
440
|
-
return { subscriptionPrice, subscriptionPeriod };
|
|
441
|
-
}
|
|
442
|
-
async function fetchActiveSubscriptionEventData(admin, logger = console) {
|
|
443
|
-
const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
|
|
444
|
-
const result = await response.json();
|
|
445
|
-
if (result.errors?.length) {
|
|
446
|
-
logger.error?.("Partner subscription query errors", { errors: result.errors });
|
|
447
|
-
}
|
|
448
|
-
const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
|
|
449
|
-
const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
|
|
450
|
-
const subscriptionId = parseSubscriptionId(active?.id);
|
|
451
|
-
if (!subscriptionId) return null;
|
|
452
|
-
let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
|
|
453
|
-
if (!subscriptionPeriod || subscriptionPrice === null) {
|
|
454
|
-
try {
|
|
455
|
-
const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
|
|
456
|
-
variables: { id: active?.id }
|
|
457
|
-
});
|
|
458
|
-
const detailsJson = await detailsResponse.json();
|
|
459
|
-
if (detailsJson.errors?.length) {
|
|
460
|
-
logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
|
|
461
|
-
}
|
|
462
|
-
const details = detailsJson.data?.node;
|
|
463
|
-
({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
|
|
464
|
-
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
465
|
-
return {
|
|
466
|
-
subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
|
|
467
|
-
subscriptionName: details?.name ?? active?.name,
|
|
468
|
-
subscriptionId,
|
|
469
|
-
subscriptionPrice,
|
|
470
|
-
subscriptionPeriod
|
|
471
|
-
};
|
|
472
|
-
} catch (error) {
|
|
473
|
-
logger.error?.("Partner subscription details query failed", {
|
|
474
|
-
error: error instanceof Error ? error.message : String(error)
|
|
475
|
-
});
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
480
|
-
return {
|
|
481
|
-
subscriptionStatus: active?.status ?? "ACTIVE",
|
|
482
|
-
subscriptionName: active?.name,
|
|
483
|
-
subscriptionId,
|
|
484
|
-
subscriptionPrice,
|
|
485
|
-
subscriptionPeriod
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
503
|
// Annotate the CommonJS export names for ESM import in node:
|
|
489
504
|
0 && (module.exports = {
|
|
490
505
|
createProgusConnector,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as
|
|
2
|
-
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-
|
|
1
|
+
import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdWithSubscriptionInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-BdqPj2g7.cjs';
|
|
2
|
+
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.cjs';
|
|
3
3
|
|
|
4
4
|
type Connector = {
|
|
5
5
|
track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
|
|
@@ -22,7 +22,7 @@ type Connector = {
|
|
|
22
22
|
shopDomain: string;
|
|
23
23
|
data: SubscriptionEventData;
|
|
24
24
|
}) => Promise<TrackResult>;
|
|
25
|
-
|
|
25
|
+
assignPartnerIdWithSubscription: (payload: AssignPartnerIdWithSubscriptionInput) => Promise<TrackResult & {
|
|
26
26
|
partnerId?: string;
|
|
27
27
|
}>;
|
|
28
28
|
checkPartnerId: (payload: {
|
|
@@ -39,4 +39,4 @@ declare function signPayload(body: string, secret: string): string;
|
|
|
39
39
|
|
|
40
40
|
declare function normalizePartnerId(value?: string | null): string | null;
|
|
41
41
|
|
|
42
|
-
export {
|
|
42
|
+
export { AssignPartnerIdWithSubscriptionInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as
|
|
2
|
-
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-
|
|
1
|
+
import { C as ConnectorConfig, T as TrackEventParams, a as TrackResult, S as SubscriptionEventData, A as AssignPartnerIdWithSubscriptionInput, b as CheckPartnerIdResult, c as ShopifyAdminGraphQLClient } from './crossSell-BdqPj2g7.js';
|
|
2
|
+
export { e as AppsCatalogEntry, h as CrossSellFetchOptions, i as CrossSellOptions, L as Logger, j as TrackEventName, f as fetchAppsCatalog, g as getCrossSellOffers, d as getCrossSellOffersFromApi } from './crossSell-BdqPj2g7.js';
|
|
3
3
|
|
|
4
4
|
type Connector = {
|
|
5
5
|
track: (eventName: TrackEventParams["eventName"], payload: Omit<TrackEventParams, "eventName">) => Promise<TrackResult>;
|
|
@@ -22,7 +22,7 @@ type Connector = {
|
|
|
22
22
|
shopDomain: string;
|
|
23
23
|
data: SubscriptionEventData;
|
|
24
24
|
}) => Promise<TrackResult>;
|
|
25
|
-
|
|
25
|
+
assignPartnerIdWithSubscription: (payload: AssignPartnerIdWithSubscriptionInput) => Promise<TrackResult & {
|
|
26
26
|
partnerId?: string;
|
|
27
27
|
}>;
|
|
28
28
|
checkPartnerId: (payload: {
|
|
@@ -39,4 +39,4 @@ declare function signPayload(body: string, secret: string): string;
|
|
|
39
39
|
|
|
40
40
|
declare function normalizePartnerId(value?: string | null): string | null;
|
|
41
41
|
|
|
42
|
-
export {
|
|
42
|
+
export { AssignPartnerIdWithSubscriptionInput, CheckPartnerIdResult, ConnectorConfig, ShopifyAdminGraphQLClient, SubscriptionEventData, TrackEventParams, TrackResult, createProgusConnector, fetchActiveSubscriptionEventData, normalizePartnerId, signPayload };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,142 @@ import {
|
|
|
8
8
|
stripTrailingSlash
|
|
9
9
|
} from "./chunk-WZ5FAA44.js";
|
|
10
10
|
|
|
11
|
+
// src/subscription.ts
|
|
12
|
+
var ACTIVE_SUBSCRIPTIONS_QUERY = `
|
|
13
|
+
query {
|
|
14
|
+
currentAppInstallation {
|
|
15
|
+
activeSubscriptions {
|
|
16
|
+
id
|
|
17
|
+
name
|
|
18
|
+
status
|
|
19
|
+
lineItems {
|
|
20
|
+
plan {
|
|
21
|
+
pricingDetails {
|
|
22
|
+
__typename
|
|
23
|
+
... on AppRecurringPricing {
|
|
24
|
+
interval
|
|
25
|
+
price {
|
|
26
|
+
amount
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
... on AppUsagePricing {
|
|
30
|
+
cappedAmount {
|
|
31
|
+
amount
|
|
32
|
+
}
|
|
33
|
+
terms
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
var SUBSCRIPTION_DETAILS_QUERY = `
|
|
43
|
+
query AppSubscriptionDetails($id: ID!) {
|
|
44
|
+
node(id: $id) {
|
|
45
|
+
... on AppSubscription {
|
|
46
|
+
id
|
|
47
|
+
name
|
|
48
|
+
status
|
|
49
|
+
lineItems {
|
|
50
|
+
plan {
|
|
51
|
+
pricingDetails {
|
|
52
|
+
__typename
|
|
53
|
+
... on AppRecurringPricing {
|
|
54
|
+
interval
|
|
55
|
+
price {
|
|
56
|
+
amount
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
... on AppUsagePricing {
|
|
60
|
+
cappedAmount {
|
|
61
|
+
amount
|
|
62
|
+
}
|
|
63
|
+
terms
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
function normalizeInterval(intervalRaw, nameHint) {
|
|
73
|
+
const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
|
|
74
|
+
if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
|
|
75
|
+
if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
|
|
76
|
+
const name = (nameHint ?? "").toLowerCase();
|
|
77
|
+
if (name.includes("annual") || name.includes("year")) return "ANNUAL";
|
|
78
|
+
if (name) return "EVERY_30_DAYS";
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
function parseSubscriptionId(rawId) {
|
|
82
|
+
if (!rawId) return null;
|
|
83
|
+
return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
|
|
84
|
+
}
|
|
85
|
+
function parsePrice(rawPrice) {
|
|
86
|
+
if (typeof rawPrice === "number") return rawPrice;
|
|
87
|
+
if (typeof rawPrice === "string") {
|
|
88
|
+
const parsed = Number(rawPrice);
|
|
89
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function extractPricing(subscription) {
|
|
94
|
+
const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
|
|
95
|
+
const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
|
|
96
|
+
const subscriptionPrice = parsePrice(rawPrice);
|
|
97
|
+
const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
|
|
98
|
+
return { subscriptionPrice, subscriptionPeriod };
|
|
99
|
+
}
|
|
100
|
+
async function fetchActiveSubscriptionEventData(admin, logger = console) {
|
|
101
|
+
const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
|
|
102
|
+
const result = await response.json();
|
|
103
|
+
if (result.errors?.length) {
|
|
104
|
+
logger.error?.("Partner subscription query errors", { errors: result.errors });
|
|
105
|
+
}
|
|
106
|
+
const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
|
|
107
|
+
const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
|
|
108
|
+
const subscriptionId = parseSubscriptionId(active?.id);
|
|
109
|
+
if (!subscriptionId) return null;
|
|
110
|
+
let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
|
|
111
|
+
if (!subscriptionPeriod || subscriptionPrice === null) {
|
|
112
|
+
try {
|
|
113
|
+
const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
|
|
114
|
+
variables: { id: active?.id }
|
|
115
|
+
});
|
|
116
|
+
const detailsJson = await detailsResponse.json();
|
|
117
|
+
if (detailsJson.errors?.length) {
|
|
118
|
+
logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
|
|
119
|
+
}
|
|
120
|
+
const details = detailsJson.data?.node;
|
|
121
|
+
({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
|
|
122
|
+
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
123
|
+
return {
|
|
124
|
+
subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
|
|
125
|
+
subscriptionName: details?.name ?? active?.name,
|
|
126
|
+
subscriptionId,
|
|
127
|
+
subscriptionPrice,
|
|
128
|
+
subscriptionPeriod
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.error?.("Partner subscription details query failed", {
|
|
132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133
|
+
});
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
138
|
+
return {
|
|
139
|
+
subscriptionStatus: active?.status ?? "ACTIVE",
|
|
140
|
+
subscriptionName: active?.name,
|
|
141
|
+
subscriptionId,
|
|
142
|
+
subscriptionPrice,
|
|
143
|
+
subscriptionPeriod
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
11
147
|
// src/signing.ts
|
|
12
148
|
import { createHmac } from "crypto";
|
|
13
149
|
function signPayload(body, secret) {
|
|
@@ -32,7 +168,7 @@ function createProgusConnector(config) {
|
|
|
32
168
|
trackSubscriptionPurchased: fail,
|
|
33
169
|
trackSubscriptionUpdated: fail,
|
|
34
170
|
trackSubscriptionCancelled: fail,
|
|
35
|
-
|
|
171
|
+
assignPartnerIdWithSubscription: fail,
|
|
36
172
|
checkPartnerId: async () => ({ success: false, message, status: 500 })
|
|
37
173
|
};
|
|
38
174
|
}
|
|
@@ -126,12 +262,27 @@ function createProgusConnector(config) {
|
|
|
126
262
|
externalId: String(normalized.subscriptionId)
|
|
127
263
|
});
|
|
128
264
|
}
|
|
129
|
-
async function
|
|
130
|
-
const
|
|
265
|
+
async function assignPartnerIdWithSubscription(payload) {
|
|
266
|
+
const { admin, shopDomain, partnerId } = payload;
|
|
267
|
+
const normalized = normalizePartnerId(partnerId);
|
|
131
268
|
if (!normalized) {
|
|
132
269
|
return { success: false, message: "Partner ID is required" };
|
|
133
270
|
}
|
|
134
|
-
const result = await trackInstall({ shopDomain
|
|
271
|
+
const result = await trackInstall({ shopDomain, partnerId: normalized });
|
|
272
|
+
if (!result.success) return { ...result, partnerId: normalized };
|
|
273
|
+
try {
|
|
274
|
+
const subscriptionPayload = await fetchActiveSubscriptionEventData(admin, logger);
|
|
275
|
+
if (subscriptionPayload) {
|
|
276
|
+
await trackSubscription({
|
|
277
|
+
shopDomain,
|
|
278
|
+
data: subscriptionPayload
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger?.error?.("Partner subscription sync failed", {
|
|
283
|
+
error: error instanceof Error ? error.message : String(error)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
135
286
|
return { ...result, partnerId: normalized };
|
|
136
287
|
}
|
|
137
288
|
async function checkPartnerId(payload) {
|
|
@@ -203,146 +354,10 @@ function createProgusConnector(config) {
|
|
|
203
354
|
trackSubscriptionPurchased: trackSubscription,
|
|
204
355
|
trackSubscriptionUpdated: trackSubscription,
|
|
205
356
|
trackSubscriptionCancelled: trackSubscription,
|
|
206
|
-
|
|
357
|
+
assignPartnerIdWithSubscription,
|
|
207
358
|
checkPartnerId
|
|
208
359
|
};
|
|
209
360
|
}
|
|
210
|
-
|
|
211
|
-
// src/subscription.ts
|
|
212
|
-
var ACTIVE_SUBSCRIPTIONS_QUERY = `
|
|
213
|
-
query {
|
|
214
|
-
currentAppInstallation {
|
|
215
|
-
activeSubscriptions {
|
|
216
|
-
id
|
|
217
|
-
name
|
|
218
|
-
status
|
|
219
|
-
lineItems {
|
|
220
|
-
plan {
|
|
221
|
-
pricingDetails {
|
|
222
|
-
__typename
|
|
223
|
-
... on AppRecurringPricing {
|
|
224
|
-
interval
|
|
225
|
-
price {
|
|
226
|
-
amount
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
... on AppUsagePricing {
|
|
230
|
-
cappedAmount {
|
|
231
|
-
amount
|
|
232
|
-
}
|
|
233
|
-
terms
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
`;
|
|
242
|
-
var SUBSCRIPTION_DETAILS_QUERY = `
|
|
243
|
-
query AppSubscriptionDetails($id: ID!) {
|
|
244
|
-
node(id: $id) {
|
|
245
|
-
... on AppSubscription {
|
|
246
|
-
id
|
|
247
|
-
name
|
|
248
|
-
status
|
|
249
|
-
lineItems {
|
|
250
|
-
plan {
|
|
251
|
-
pricingDetails {
|
|
252
|
-
__typename
|
|
253
|
-
... on AppRecurringPricing {
|
|
254
|
-
interval
|
|
255
|
-
price {
|
|
256
|
-
amount
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
... on AppUsagePricing {
|
|
260
|
-
cappedAmount {
|
|
261
|
-
amount
|
|
262
|
-
}
|
|
263
|
-
terms
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
`;
|
|
272
|
-
function normalizeInterval(intervalRaw, nameHint) {
|
|
273
|
-
const interval = intervalRaw ? String(intervalRaw).toUpperCase() : "";
|
|
274
|
-
if (interval === "ANNUAL" || interval === "EVERY_30_DAYS") return interval;
|
|
275
|
-
if (interval === "MONTHLY" || interval === "EVERY_30DAYS") return "EVERY_30_DAYS";
|
|
276
|
-
const name = (nameHint ?? "").toLowerCase();
|
|
277
|
-
if (name.includes("annual") || name.includes("year")) return "ANNUAL";
|
|
278
|
-
if (name) return "EVERY_30_DAYS";
|
|
279
|
-
return "";
|
|
280
|
-
}
|
|
281
|
-
function parseSubscriptionId(rawId) {
|
|
282
|
-
if (!rawId) return null;
|
|
283
|
-
return rawId.match(/\d+$/)?.[0] ?? rawId.split("/").pop() ?? rawId;
|
|
284
|
-
}
|
|
285
|
-
function parsePrice(rawPrice) {
|
|
286
|
-
if (typeof rawPrice === "number") return rawPrice;
|
|
287
|
-
if (typeof rawPrice === "string") {
|
|
288
|
-
const parsed = Number(rawPrice);
|
|
289
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
function extractPricing(subscription) {
|
|
294
|
-
const pricingDetails = subscription?.lineItems?.[0]?.plan?.pricingDetails;
|
|
295
|
-
const rawPrice = pricingDetails?.price?.amount ?? pricingDetails?.cappedAmount?.amount;
|
|
296
|
-
const subscriptionPrice = parsePrice(rawPrice);
|
|
297
|
-
const subscriptionPeriod = normalizeInterval(pricingDetails?.interval, subscription?.name);
|
|
298
|
-
return { subscriptionPrice, subscriptionPeriod };
|
|
299
|
-
}
|
|
300
|
-
async function fetchActiveSubscriptionEventData(admin, logger = console) {
|
|
301
|
-
const response = await admin.graphql(ACTIVE_SUBSCRIPTIONS_QUERY);
|
|
302
|
-
const result = await response.json();
|
|
303
|
-
if (result.errors?.length) {
|
|
304
|
-
logger.error?.("Partner subscription query errors", { errors: result.errors });
|
|
305
|
-
}
|
|
306
|
-
const subscriptions = result.data?.currentAppInstallation?.activeSubscriptions ?? [];
|
|
307
|
-
const active = subscriptions.find((sub) => String(sub.status ?? "").toUpperCase() === "ACTIVE") ?? subscriptions[0];
|
|
308
|
-
const subscriptionId = parseSubscriptionId(active?.id);
|
|
309
|
-
if (!subscriptionId) return null;
|
|
310
|
-
let { subscriptionPrice, subscriptionPeriod } = extractPricing(active);
|
|
311
|
-
if (!subscriptionPeriod || subscriptionPrice === null) {
|
|
312
|
-
try {
|
|
313
|
-
const detailsResponse = await admin.graphql(SUBSCRIPTION_DETAILS_QUERY, {
|
|
314
|
-
variables: { id: active?.id }
|
|
315
|
-
});
|
|
316
|
-
const detailsJson = await detailsResponse.json();
|
|
317
|
-
if (detailsJson.errors?.length) {
|
|
318
|
-
logger.error?.("Partner subscription details query errors", { errors: detailsJson.errors });
|
|
319
|
-
}
|
|
320
|
-
const details = detailsJson.data?.node;
|
|
321
|
-
({ subscriptionPrice, subscriptionPeriod } = extractPricing(details));
|
|
322
|
-
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
323
|
-
return {
|
|
324
|
-
subscriptionStatus: details?.status ?? active?.status ?? "ACTIVE",
|
|
325
|
-
subscriptionName: details?.name ?? active?.name,
|
|
326
|
-
subscriptionId,
|
|
327
|
-
subscriptionPrice,
|
|
328
|
-
subscriptionPeriod
|
|
329
|
-
};
|
|
330
|
-
} catch (error) {
|
|
331
|
-
logger.error?.("Partner subscription details query failed", {
|
|
332
|
-
error: error instanceof Error ? error.message : String(error)
|
|
333
|
-
});
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (!subscriptionPeriod || subscriptionPrice === null) return null;
|
|
338
|
-
return {
|
|
339
|
-
subscriptionStatus: active?.status ?? "ACTIVE",
|
|
340
|
-
subscriptionName: active?.name,
|
|
341
|
-
subscriptionId,
|
|
342
|
-
subscriptionPrice,
|
|
343
|
-
subscriptionPeriod
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
361
|
export {
|
|
347
362
|
createProgusConnector,
|
|
348
363
|
fetchActiveSubscriptionEventData,
|