@itm-studio/partner-sdk 0.3.6 → 0.3.7

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 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 paginated tickets for a moment.
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.
@@ -2021,3 +2258,38 @@ Each requested revision gets a new reservation and brand media asset.
2021
2258
  Deploy the backend reservation migration and schema before publishing SDK 0.3.3,
2022
2259
  then release the Eve and Backstage consumers. These operations do not call an
2023
2260
  image provider or consume notification credits.
2261
+
2262
+
2263
+ ### Record an off-site contribution
2264
+
2265
+ Partner tokens and brand-agent sessions can use `getPartnerContributionPages` to
2266
+ choose a page, then log money already received outside ITM. The backend derives
2267
+ brand scope from authentication. Name and amount are required; email, phone (with
2268
+ country code), and note are optional. This creates no charge, refund capability,
2269
+ new account, or automatic receipt. Paused pages accept manual records; archived
2270
+ pages reject new records. Amounts use ITM cents (major units × 100, including JPY).
2271
+
2272
+ ```ts
2273
+ const result = await itm.mutation({
2274
+ createPartnerManualContribution: {
2275
+ __args: { input: {
2276
+ contributionPageUid: pageUid,
2277
+ currencyCode: pageCurrencyCode, // Prevent stale page currency from changing the amount.
2278
+ requestUid, // Generate once and retain unchanged across retries.
2279
+ name: 'Alex',
2280
+ amountCents: 2500,
2281
+ } },
2282
+ uid: true,
2283
+ source: true, // OFF_SITE
2284
+ status: true, // SUCCEEDED
2285
+ },
2286
+ });
2287
+ ```
2288
+
2289
+ `ContributionAdmin.donorEmail` is nullable for name-only manual entries.
2290
+ Set `sendReceipt: true` with an `email` to request a receipt. The backend chooses
2291
+ and freezes `manualReceiptType` (`REGULAR` or `DONATION`) from the brand's current
2292
+ receipt eligibility. Omit it for no receipt. `userUid` is populated when entered
2293
+ email or phone matches a current verified ITM identity; unmatched contacts do not
2294
+ create an account, and conflicting matches are rejected. `source` distinguishes
2295
+ `STRIPE`, `OFF_SITE`, and in-person `POS` independently of payment status.