@itm-studio/partner-sdk 0.3.6 → 0.3.8
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 +342 -1
- package/dist/index.d.mts +358 -4
- package/dist/index.d.ts +358 -4
- package/dist/index.js +700 -260
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +700 -260
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -1
- package/schema/partner.graphql +270 -2
package/README.md
CHANGED
|
@@ -47,6 +47,11 @@ const { getPartnerMomentsForBrand } = await itm.query({
|
|
|
47
47
|
city: true,
|
|
48
48
|
country: true,
|
|
49
49
|
},
|
|
50
|
+
publicVenuePreview: {
|
|
51
|
+
city: true,
|
|
52
|
+
country: true,
|
|
53
|
+
countryCode: true,
|
|
54
|
+
},
|
|
50
55
|
ticketTiers: {
|
|
51
56
|
name: true,
|
|
52
57
|
price: true,
|
|
@@ -647,6 +652,11 @@ const { getPartnerMoment } = await itm.query({
|
|
|
647
652
|
city: true,
|
|
648
653
|
postalCode: true,
|
|
649
654
|
},
|
|
655
|
+
publicVenuePreview: {
|
|
656
|
+
city: true,
|
|
657
|
+
country: true,
|
|
658
|
+
countryCode: true,
|
|
659
|
+
},
|
|
650
660
|
ticketTiers: {
|
|
651
661
|
name: true,
|
|
652
662
|
price: true,
|
|
@@ -656,6 +666,15 @@ const { getPartnerMoment } = await itm.query({
|
|
|
656
666
|
});
|
|
657
667
|
```
|
|
658
668
|
|
|
669
|
+
`Moment.venue` returns full venue details and is nullable. For public location
|
|
670
|
+
text, select `Moment.publicVenuePreview`: it returns `city`, a display `country`
|
|
671
|
+
label, and nullable `countryCode`, without the venue name or street address. It
|
|
672
|
+
remains available when the venue is hidden until RSVP, and returns `null` when
|
|
673
|
+
no venue is set. The API field is named `publicVenuePreview`.
|
|
674
|
+
|
|
675
|
+
The same selection works wherever the API returns a `Moment`, including
|
|
676
|
+
`getPartnerMomentsForBrand.moments` and `Ticket.moment`.
|
|
677
|
+
|
|
659
678
|
### `getPublicMomentLineups`
|
|
660
679
|
|
|
661
680
|
Get approved public lineup entries for a moment. Use `getPartnerMoment` first if you only have the moment slug; this query takes the moment `uid`.
|
|
@@ -860,13 +879,20 @@ Supported filters include `ticketTierUids`, `search`, `isActive`, `isPrivate`,
|
|
|
860
879
|
|
|
861
880
|
### `getPartnerTicketsForMoment`
|
|
862
881
|
|
|
863
|
-
Get
|
|
882
|
+
Get confirmed tickets for a moment owned by your partner token's brand. The
|
|
883
|
+
`user` filter matches the current ITM account holding the ticket, rather than
|
|
884
|
+
guest contact details entered at checkout. Provide exactly one of `userUid`,
|
|
885
|
+
`email`, or `phoneNumber`. An unknown account returns an empty ticket page.
|
|
886
|
+
|
|
887
|
+
`take` defaults to 50 and is capped at 100. Continue with `nextCursor` while
|
|
888
|
+
`hasNextPage` is true to retrieve all tickets.
|
|
864
889
|
|
|
865
890
|
```typescript
|
|
866
891
|
const { getPartnerTicketsForMoment } = await itm.query({
|
|
867
892
|
getPartnerTicketsForMoment: {
|
|
868
893
|
__args: {
|
|
869
894
|
momentSlug: "my-event-slug",
|
|
895
|
+
user: { userUid: "customer-uid" }, // optional: exactly one of userUid, email, phoneNumber
|
|
870
896
|
checkedIn: true, // optional: filter by check-in status
|
|
871
897
|
take: 50,
|
|
872
898
|
},
|
|
@@ -874,6 +900,7 @@ const { getPartnerTicketsForMoment } = await itm.query({
|
|
|
874
900
|
uid: true,
|
|
875
901
|
price: true,
|
|
876
902
|
redeemedAt: true,
|
|
903
|
+
encryptedQRCode: true, // render this exact payload as a QR image
|
|
877
904
|
hasExpired: true,
|
|
878
905
|
user: { uid: true, name: true, primaryEmail: true },
|
|
879
906
|
ticketTier: { name: true, price: true },
|
|
@@ -886,6 +913,105 @@ const { getPartnerTicketsForMoment } = await itm.query({
|
|
|
886
913
|
});
|
|
887
914
|
```
|
|
888
915
|
|
|
916
|
+
#### Displaying a purchased ITM ticket in a web or mobile app
|
|
917
|
+
|
|
918
|
+
`Ticket.encryptedQRCode` is the exact opaque payload used by ITM scanners. It is
|
|
919
|
+
a string to encode as a QR image, not an image URL. Render it unchanged: do not
|
|
920
|
+
decrypt it, wrap it in JSON, replace it with the ticket UID, or turn it into a
|
|
921
|
+
URL. Your app does not need ITM's QR encryption key.
|
|
922
|
+
|
|
923
|
+
Keep the SDK and partner token on your Node server. Authenticate the app user
|
|
924
|
+
and resolve their verified ITM account association on that server, then use
|
|
925
|
+
that account's UID (or verified email/phone) in the `user` filter. Do not trust
|
|
926
|
+
an account identifier supplied by the app client: partner tokens grant access
|
|
927
|
+
to the brand's tickets and do not authenticate individual attendees. The token
|
|
928
|
+
only retrieves tickets for its own brand's moments.
|
|
929
|
+
|
|
930
|
+
For example, install `qrcode` in your server application (`npm install qrcode`,
|
|
931
|
+
plus `npm install -D @types/qrcode` for TypeScript), and generate a PNG data URL:
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
import QRCode from "qrcode";
|
|
935
|
+
import { createITMPartnerClient } from "@itm-studio/partner-sdk";
|
|
936
|
+
|
|
937
|
+
const itm = createITMPartnerClient({
|
|
938
|
+
token: process.env.ITM_PARTNER_TOKEN!,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Call only after resolving itmUserUid from the authenticated user's verified
|
|
942
|
+
// account association on your server. It must not come from request parameters.
|
|
943
|
+
async function loadMyMomentTickets(
|
|
944
|
+
itmUserUid: string,
|
|
945
|
+
momentSlug: string,
|
|
946
|
+
cursor?: string,
|
|
947
|
+
) {
|
|
948
|
+
const { getPartnerTicketsForMoment: page } = await itm.query({
|
|
949
|
+
getPartnerTicketsForMoment: {
|
|
950
|
+
__args: {
|
|
951
|
+
momentSlug,
|
|
952
|
+
user: { userUid: itmUserUid },
|
|
953
|
+
take: 50,
|
|
954
|
+
...(cursor ? { cursor } : {}),
|
|
955
|
+
},
|
|
956
|
+
tickets: {
|
|
957
|
+
uid: true,
|
|
958
|
+
encryptedQRCode: true,
|
|
959
|
+
redeemedAt: true,
|
|
960
|
+
hasExpired: true,
|
|
961
|
+
ticketTier: { name: true },
|
|
962
|
+
moment: {
|
|
963
|
+
name: true,
|
|
964
|
+
startDate: true,
|
|
965
|
+
timezone: true,
|
|
966
|
+
publicVenuePreview: { city: true, country: true, countryCode: true },
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
hasNextPage: true,
|
|
970
|
+
nextCursor: true,
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
tickets: await Promise.all(page.tickets.map(async (ticket) => ({
|
|
976
|
+
uid: ticket.uid,
|
|
977
|
+
ticketTierName: ticket.ticketTier.name,
|
|
978
|
+
moment: ticket.moment,
|
|
979
|
+
redeemedAt: ticket.redeemedAt,
|
|
980
|
+
hasExpired: ticket.hasExpired,
|
|
981
|
+
qrCodeImageUrl: await QRCode.toDataURL(ticket.encryptedQRCode, {
|
|
982
|
+
errorCorrectionLevel: "H",
|
|
983
|
+
margin: 4,
|
|
984
|
+
width: 300,
|
|
985
|
+
}),
|
|
986
|
+
}))),
|
|
987
|
+
hasNextPage: page.hasNextPage,
|
|
988
|
+
nextCursor: page.nextCursor,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Return this page only to the authenticated owner, with `Cache-Control: private,
|
|
994
|
+
no-store`. A web app can display `ticket.qrCodeImageUrl` as an image:
|
|
995
|
+
|
|
996
|
+
```tsx
|
|
997
|
+
<img src={ticket.qrCodeImageUrl} alt="ITM ticket QR code" width={300} height={300} />
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
A mobile app can display the same PNG data URL using its image component.
|
|
1001
|
+
Alternatively, return `encryptedQRCode` only to the authenticated owner and
|
|
1002
|
+
pass it unchanged to the app's QR component. Preserve a square image, black
|
|
1003
|
+
modules on a white background, and the white margin; avoid cropping or overlays.
|
|
1004
|
+
|
|
1005
|
+
Fetch fresh ticket data whenever the ticket screen opens or regains focus.
|
|
1006
|
+
Transfers can change the holder and rotate the QR payload, and cancelled or
|
|
1007
|
+
refunded tickets no longer appear in this confirmed-ticket query. Avoid shared
|
|
1008
|
+
caches and application logs containing ticket payloads or images.
|
|
1009
|
+
|
|
1010
|
+
Displaying a QR is separate from admission validation. ITM's scanner checks
|
|
1011
|
+
the current ticket and its applicable admission rules at scan time. Display
|
|
1012
|
+
`redeemedAt` and `hasExpired` as ticket metadata; they are not a substitute for
|
|
1013
|
+
scanner validation, especially for tickets with multiple admissions.
|
|
1014
|
+
|
|
889
1015
|
### `getPartnerCustomersForMoment`
|
|
890
1016
|
|
|
891
1017
|
Get paginated customers (users with their tickets) for a moment.
|
|
@@ -954,6 +1080,117 @@ pages and optional exact counts can be up to 30 seconds old. Consent and
|
|
|
954
1080
|
suppression fields reflect ITM's current brand-scoped records but do not replace
|
|
955
1081
|
your legal-purpose and communications-compliance checks.
|
|
956
1082
|
|
|
1083
|
+
### Membership tiers and members
|
|
1084
|
+
|
|
1085
|
+
`getPartnerMembershipTiers` and `getPartnerMembers` use your partner token to
|
|
1086
|
+
select the brand; neither accepts a brand ID. Both queries use optional `input`
|
|
1087
|
+
with `cursor` and `take` (default 50, maximum 100), returning `totalCount`,
|
|
1088
|
+
`hasNextPage`, and `nextCursor`. These operations require a partner token;
|
|
1089
|
+
adding them to the SDK does not grant access to agent sessions.
|
|
1090
|
+
|
|
1091
|
+
#### `getPartnerMembershipTiers`: fetch every tier
|
|
1092
|
+
|
|
1093
|
+
Unfiltered results include inactive and private tiers, but exclude soft-deleted
|
|
1094
|
+
tiers. Optional filters are `search` (name or slug), `isActive`, `isPublic`, and
|
|
1095
|
+
`passTypes`. Tiers are ordered by `sortOrder`, then creation order.
|
|
1096
|
+
|
|
1097
|
+
```typescript
|
|
1098
|
+
import type { GetPartnerMembershipTiersInput } from "@itm-studio/partner-sdk";
|
|
1099
|
+
|
|
1100
|
+
let tierCursor: string | null = null;
|
|
1101
|
+
do {
|
|
1102
|
+
const tierInput: GetPartnerMembershipTiersInput = { cursor: tierCursor, take: 100 };
|
|
1103
|
+
const { getPartnerMembershipTiers: page } = await itm.query({
|
|
1104
|
+
getPartnerMembershipTiers: {
|
|
1105
|
+
__args: { input: tierInput },
|
|
1106
|
+
membershipTiers: {
|
|
1107
|
+
uid: true, name: true, slug: true, passType: true,
|
|
1108
|
+
currencyCode: true, price: true, yearlyPrice: true, oneTimePrice: true,
|
|
1109
|
+
isActive: true, isPublic: true, activeMemberCount: true,
|
|
1110
|
+
},
|
|
1111
|
+
totalCount: true, hasNextPage: true, nextCursor: true,
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
// Process or persist page.membershipTiers on your server.
|
|
1115
|
+
if (!page.hasNextPage) break;
|
|
1116
|
+
if (!page.nextCursor || page.nextCursor === tierCursor) {
|
|
1117
|
+
throw new Error("Missing or repeated membership tier cursor");
|
|
1118
|
+
}
|
|
1119
|
+
tierCursor = page.nextCursor;
|
|
1120
|
+
} while (true);
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
Tier `price`, `yearlyPrice`, and `oneTimePrice` are stored in hundredths of the
|
|
1124
|
+
currency's major unit, including JPY: divide by 100 to display a major-unit
|
|
1125
|
+
amount. These are ITM prices, not Stripe minor-unit amounts.
|
|
1126
|
+
`activeMemberCount` counts ACTIVE and PAST_DUE membership rows, excluding deleted
|
|
1127
|
+
users; it is not a count of distinct people.
|
|
1128
|
+
|
|
1129
|
+
#### `getPartnerMembers`: fetch brand-wide or selected tiers
|
|
1130
|
+
|
|
1131
|
+
Each `members` row represents one membership, identified by its `uid`, with
|
|
1132
|
+
`customer` and `membershipTier` details. A customer may appear more than once;
|
|
1133
|
+
use `customer.uid` to group people when needed. It matches the customer UID
|
|
1134
|
+
returned by `getPartnerAudience`. Members are ordered newest first.
|
|
1135
|
+
|
|
1136
|
+
```typescript
|
|
1137
|
+
import type { GetPartnerMembersInput } from "@itm-studio/partner-sdk";
|
|
1138
|
+
|
|
1139
|
+
// Leave empty for the entire brand. To select tiers, use:
|
|
1140
|
+
// { membershipTierUids: ["tier-uid"] }
|
|
1141
|
+
const memberFilters: GetPartnerMembersInput = {};
|
|
1142
|
+
let memberCursor: string | null = null;
|
|
1143
|
+
do {
|
|
1144
|
+
const memberInput: GetPartnerMembersInput = { ...memberFilters, cursor: memberCursor, take: 100 };
|
|
1145
|
+
const { getPartnerMembers: page } = await itm.query({
|
|
1146
|
+
getPartnerMembers: {
|
|
1147
|
+
__args: { input: memberInput },
|
|
1148
|
+
members: {
|
|
1149
|
+
uid: true, status: true, source: true, membershipNumber: true,
|
|
1150
|
+
quantity: true, purchaseType: true, issuedAt: true,
|
|
1151
|
+
currentPeriodStart: true, currentPeriodEnd: true,
|
|
1152
|
+
cancelAtPeriodEnd: true, remainingUses: true,
|
|
1153
|
+
customer: { uid: true, name: true, email: true, emailVerified: true, phoneNumber: true },
|
|
1154
|
+
membershipTier: { uid: true, name: true, slug: true, passType: true },
|
|
1155
|
+
},
|
|
1156
|
+
totalCount: true, hasNextPage: true, nextCursor: true,
|
|
1157
|
+
},
|
|
1158
|
+
});
|
|
1159
|
+
// Process or persist page.members on your server.
|
|
1160
|
+
if (!page.hasNextPage) break;
|
|
1161
|
+
if (!page.nextCursor || page.nextCursor === memberCursor) {
|
|
1162
|
+
throw new Error("Missing or repeated membership cursor");
|
|
1163
|
+
}
|
|
1164
|
+
memberCursor = page.nextCursor;
|
|
1165
|
+
} while (true);
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
The default includes ACTIVE, PAST_DUE, CANCELLED, EXPIRED, and REFUNDED.
|
|
1169
|
+
PENDING represents unpaid checkout reservations and is excluded unless requested
|
|
1170
|
+
explicitly. To include every status, set `memberFilters.statuses` to
|
|
1171
|
+
`["ACTIVE", "PAST_DUE", "CANCELLED", "EXPIRED", "REFUNDED", "PENDING"]`.
|
|
1172
|
+
An empty or null statuses list also uses the default.
|
|
1173
|
+
|
|
1174
|
+
Additional filters include `search` (name, email, phone, or exact membership
|
|
1175
|
+
number), `sources`, `customerUids`, `cancelAtPeriodEnd`, `createdAtFrom`,
|
|
1176
|
+
`createdAtBefore`, `currentPeriodEndFrom`, and `currentPeriodEndBefore`.
|
|
1177
|
+
`From` dates are inclusive and `Before` dates exclusive. Keep filters identical
|
|
1178
|
+
across pages. Tier and customer UID lists accept at most 100 distinct values;
|
|
1179
|
+
search accepts at most 100 characters. Foreign and nonexistent tier UIDs return
|
|
1180
|
+
the same error.
|
|
1181
|
+
|
|
1182
|
+
Deleted users are excluded. Memberships on soft-deleted tiers remain available,
|
|
1183
|
+
so fetch the brand-wide member listing directly rather than assembling it by
|
|
1184
|
+
looping over listed tiers. Pagination reads live data without a snapshot
|
|
1185
|
+
guarantee: changes during a walk, especially tier reordering, can omit or repeat
|
|
1186
|
+
rows. `remainingUses: null` means unlimited; a null `currentPeriodEnd` alone does
|
|
1187
|
+
not establish whether a membership has no expiry.
|
|
1188
|
+
|
|
1189
|
+
The partner responses expose the allowlisted customer, membership, and tier
|
|
1190
|
+
fields, excluding QR tokens, Stripe/payment identifiers, guest information, and
|
|
1191
|
+
form responses. Both endpoints are limited to 120 requests per minute per
|
|
1192
|
+
partner token.
|
|
1193
|
+
|
|
957
1194
|
### `getPartnerMomentCollection`
|
|
958
1195
|
|
|
959
1196
|
Get a moment collection by slug. Collections group related moments together (e.g. a summer series, a festival lineup). Returns the collection with its moments and links.
|
|
@@ -1344,6 +1581,75 @@ await itm.mutation({
|
|
|
1344
1581
|
});
|
|
1345
1582
|
```
|
|
1346
1583
|
|
|
1584
|
+
### Create a subscribe drop and capture signups without OTP
|
|
1585
|
+
|
|
1586
|
+
Use a **partner API token on your server**. First create a drop, then pass its UID
|
|
1587
|
+
with each signup. These mutations use the brand associated with the token.
|
|
1588
|
+
|
|
1589
|
+
```ts
|
|
1590
|
+
const { createPartnerBrandDrop: drop } = await itm.mutation({
|
|
1591
|
+
createPartnerBrandDrop: {
|
|
1592
|
+
__args: {
|
|
1593
|
+
input: {
|
|
1594
|
+
name: 'Join our community',
|
|
1595
|
+
ctaType: 'SUBSCRIBE',
|
|
1596
|
+
coverImageUid: 'your-brand-image-uid',
|
|
1597
|
+
isPublic: true,
|
|
1598
|
+
captureName: true,
|
|
1599
|
+
captureEmail: true,
|
|
1600
|
+
consentText: 'I agree to receive updates from this brand.',
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
uid: true,
|
|
1604
|
+
slug: true,
|
|
1605
|
+
},
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
const { createPartnerBrandSubscription: signup } = await itm.mutation({
|
|
1609
|
+
createPartnerBrandSubscription: {
|
|
1610
|
+
__args: {
|
|
1611
|
+
input: {
|
|
1612
|
+
dropUid: drop.uid,
|
|
1613
|
+
fullName: 'Jane Subscriber',
|
|
1614
|
+
email: 'jane@example.com',
|
|
1615
|
+
phoneNumber: '+14155550123',
|
|
1616
|
+
requireOtpVerification: false, // Also the default when omitted.
|
|
1617
|
+
compliance: {
|
|
1618
|
+
pageUrl: 'https://example.com/join',
|
|
1619
|
+
submissionMethod: 'partner_sdk',
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
verificationRequired: true,
|
|
1624
|
+
subscription: { uid: true, isActive: true },
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
// signup.verificationRequired === false; no OTP or user login step is required.
|
|
1628
|
+
```
|
|
1629
|
+
|
|
1630
|
+
- `createPartnerBrandDrop` supports `ctaType: 'SUBSCRIBE'`. Upload/register an
|
|
1631
|
+
image with `getPartnerUploadUrl` and `createPartnerMediaAsset`, or select an
|
|
1632
|
+
existing image with `getPartnerMediaAssets`. Both cover images must be active
|
|
1633
|
+
assets owned by this brand. Save the returned drop UID and reuse it for signups;
|
|
1634
|
+
retrying drop creation creates another drop.
|
|
1635
|
+
- `dropUid` is optional. Supplying it creates both the brand subscription and the
|
|
1636
|
+
drop attribution, using the existing drop consent, list, and trigger flow.
|
|
1637
|
+
Retrying a signup reuses the subscription and its drop attribution.
|
|
1638
|
+
- The drop must belong to the token's brand, be undeleted, and use `SUBSCRIBE`.
|
|
1639
|
+
Required name/email capture still applies. Full names need first and last name;
|
|
1640
|
+
emails are normalized and phones use E.164. Existing drops with required custom
|
|
1641
|
+
checkbox or guest-information fields still enforce those requirements; this
|
|
1642
|
+
simple signup input does not collect those additional fields.
|
|
1643
|
+
- Contact detail synchronization runs asynchronously. Account email ownership
|
|
1644
|
+
remains subject to the existing profile verification/conflict rules; it does
|
|
1645
|
+
not block creation of the subscription. Subscription capture does not verify
|
|
1646
|
+
the person's identity.
|
|
1647
|
+
- Only submit people who opted in. The drop's configured `consentText` is stored
|
|
1648
|
+
with its attribution. Supply the subscriber's actual compliance metadata when
|
|
1649
|
+
available; otherwise request metadata describes your server.
|
|
1650
|
+
- Both mutations require a partner token (agent sessions are not accepted) and
|
|
1651
|
+
are limited to 30 requests per minute. Queue larger imports accordingly.
|
|
1652
|
+
|
|
1347
1653
|
### `createPartnerBrandSubscription`
|
|
1348
1654
|
|
|
1349
1655
|
Subscribe a phone number to the authenticated partner brand.
|
|
@@ -2021,3 +2327,38 @@ Each requested revision gets a new reservation and brand media asset.
|
|
|
2021
2327
|
Deploy the backend reservation migration and schema before publishing SDK 0.3.3,
|
|
2022
2328
|
then release the Eve and Backstage consumers. These operations do not call an
|
|
2023
2329
|
image provider or consume notification credits.
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
### Record an off-site contribution
|
|
2333
|
+
|
|
2334
|
+
Partner tokens and brand-agent sessions can use `getPartnerContributionPages` to
|
|
2335
|
+
choose a page, then log money already received outside ITM. The backend derives
|
|
2336
|
+
brand scope from authentication. Name and amount are required; email, phone (with
|
|
2337
|
+
country code), and note are optional. This creates no charge, refund capability,
|
|
2338
|
+
new account, or automatic receipt. Paused pages accept manual records; archived
|
|
2339
|
+
pages reject new records. Amounts use ITM cents (major units × 100, including JPY).
|
|
2340
|
+
|
|
2341
|
+
```ts
|
|
2342
|
+
const result = await itm.mutation({
|
|
2343
|
+
createPartnerManualContribution: {
|
|
2344
|
+
__args: { input: {
|
|
2345
|
+
contributionPageUid: pageUid,
|
|
2346
|
+
currencyCode: pageCurrencyCode, // Prevent stale page currency from changing the amount.
|
|
2347
|
+
requestUid, // Generate once and retain unchanged across retries.
|
|
2348
|
+
name: 'Alex',
|
|
2349
|
+
amountCents: 2500,
|
|
2350
|
+
} },
|
|
2351
|
+
uid: true,
|
|
2352
|
+
source: true, // OFF_SITE
|
|
2353
|
+
status: true, // SUCCEEDED
|
|
2354
|
+
},
|
|
2355
|
+
});
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
`ContributionAdmin.donorEmail` is nullable for name-only manual entries.
|
|
2359
|
+
Set `sendReceipt: true` with an `email` to request a receipt. The backend chooses
|
|
2360
|
+
and freezes `manualReceiptType` (`REGULAR` or `DONATION`) from the brand's current
|
|
2361
|
+
receipt eligibility. Omit it for no receipt. `userUid` is populated when entered
|
|
2362
|
+
email or phone matches a current verified ITM identity; unmatched contacts do not
|
|
2363
|
+
create an account, and conflicting matches are rejected. `source` distinguishes
|
|
2364
|
+
`STRIPE`, `OFF_SITE`, and in-person `POS` independently of payment status.
|