sbcwallet 0.0.1

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.
Files changed (67) hide show
  1. package/.env.example +10 -0
  2. package/.github/workflows/build.yml +66 -0
  3. package/.github/workflows/release.yml +57 -0
  4. package/APPLE_WALLET_SETUP.md +318 -0
  5. package/GOOGLE_WALLET_SETUP.md +473 -0
  6. package/LICENSE +201 -0
  7. package/README.md +187 -0
  8. package/dist/adapters/apple.d.ts +10 -0
  9. package/dist/adapters/apple.js +153 -0
  10. package/dist/adapters/google.d.ts +26 -0
  11. package/dist/adapters/google.js +431 -0
  12. package/dist/api/unified.d.ts +67 -0
  13. package/dist/api/unified.js +375 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.js +11 -0
  16. package/dist/profiles/healthcare/index.d.ts +91 -0
  17. package/dist/profiles/healthcare/index.js +151 -0
  18. package/dist/profiles/logistics/index.d.ts +91 -0
  19. package/dist/profiles/logistics/index.js +152 -0
  20. package/dist/profiles/loyalty/index.d.ts +91 -0
  21. package/dist/profiles/loyalty/index.js +81 -0
  22. package/dist/templates/apple/child.json +59 -0
  23. package/dist/templates/apple/parent.json +54 -0
  24. package/dist/templates/google/child_object.json +38 -0
  25. package/dist/templates/google/loyalty_class.json +7 -0
  26. package/dist/templates/google/loyalty_object.json +29 -0
  27. package/dist/templates/google/parent_class.json +10 -0
  28. package/dist/templates/google/parent_object.json +33 -0
  29. package/dist/types.d.ts +422 -0
  30. package/dist/types.js +80 -0
  31. package/dist/utils/progress-image.d.ts +23 -0
  32. package/dist/utils/progress-image.js +94 -0
  33. package/examples/.loyalty-fixed-state.json +10 -0
  34. package/examples/claim-flow.ts +163 -0
  35. package/examples/loyalty-admin-server.js +207 -0
  36. package/examples/loyalty-admin.html +260 -0
  37. package/examples/loyalty-fixed-card-server.js +288 -0
  38. package/examples/loyalty-flow.ts +78 -0
  39. package/examples/loyalty-google-issue.js +115 -0
  40. package/package.json +51 -0
  41. package/scripts/copy-assets.js +35 -0
  42. package/scripts/smoke-dist-import.js +39 -0
  43. package/setup-google-class.js +97 -0
  44. package/setup-google-class.ts +105 -0
  45. package/src/adapters/apple.ts +193 -0
  46. package/src/adapters/google.ts +521 -0
  47. package/src/api/unified.ts +487 -0
  48. package/src/index.ts +74 -0
  49. package/src/profiles/healthcare/index.ts +157 -0
  50. package/src/profiles/logistics/index.ts +158 -0
  51. package/src/profiles/loyalty/index.ts +87 -0
  52. package/src/templates/apple/child.json +59 -0
  53. package/src/templates/apple/parent.json +54 -0
  54. package/src/templates/google/child_object.json +38 -0
  55. package/src/templates/google/loyalty_class.json +7 -0
  56. package/src/templates/google/loyalty_object.json +29 -0
  57. package/src/templates/google/parent_class.json +10 -0
  58. package/src/templates/google/parent_object.json +33 -0
  59. package/src/types.ts +324 -0
  60. package/src/utils/progress-image.ts +130 -0
  61. package/test-google-wallet.js +78 -0
  62. package/test-google-wallet.ts +94 -0
  63. package/tests/adapters.test.ts +244 -0
  64. package/tests/loyalty.test.ts +39 -0
  65. package/tests/unified.test.ts +388 -0
  66. package/tsconfig.json +19 -0
  67. package/vitest.config.ts +12 -0
package/src/types.ts ADDED
@@ -0,0 +1,324 @@
1
+ import { z } from 'zod'
2
+
3
+ // Profile types
4
+ export type ProfileType = 'logistics' | 'healthcare' | 'loyalty'
5
+
6
+ // Status types
7
+ export type LogisticsStatus = 'ISSUED' | 'PRESENCE' | 'SCALE' | 'OPS' | 'EXITED'
8
+ export type HealthcareStatus = 'SCHEDULED' | 'CHECKIN' | 'PROCEDURE' | 'DISCHARGED'
9
+ export type LoyaltyStatus = 'ACTIVE' | 'SUSPENDED'
10
+ export type PassStatus = LogisticsStatus | HealthcareStatus | LoyaltyStatus
11
+
12
+ // Time window schema
13
+ export const TimeWindowSchema = z.object({
14
+ from: z.string(),
15
+ to: z.string(),
16
+ tz: z.string().optional()
17
+ })
18
+
19
+ export type TimeWindow = z.infer<typeof TimeWindowSchema>
20
+
21
+ // Base pass data
22
+ export interface BasePassData {
23
+ id: string
24
+ profile: ProfileType
25
+ createdAt: string
26
+ updatedAt: string
27
+ status: PassStatus
28
+ hash?: string
29
+ signature?: string
30
+ anchorId?: string
31
+ }
32
+
33
+ // Parent pass data (PES or AppointmentBatch)
34
+ export interface ParentPassData extends BasePassData {
35
+ type: 'parent'
36
+ programName: string
37
+ site?: string
38
+ window?: TimeWindow
39
+ capacity?: number
40
+ metadata?: Record<string, any>
41
+ }
42
+
43
+ // Child pass data (TO or PatientVisit)
44
+ export interface ChildPassData extends BasePassData {
45
+ type: 'child'
46
+ parentId: string
47
+ // Logistics specific
48
+ plate?: string
49
+ carrier?: string
50
+ client?: string
51
+ // Healthcare specific
52
+ patientName?: string
53
+ procedure?: string
54
+ doctor?: string
55
+
56
+ // Loyalty specific
57
+ businessId?: string
58
+ customerId?: string
59
+ customerName?: string
60
+ memberId?: string
61
+ points?: number
62
+ metadata?: Record<string, any>
63
+ }
64
+
65
+ // Unified pass data
66
+ export type PassData = ParentPassData | ChildPassData
67
+
68
+ // Create parent input schema
69
+ export const CreateParentInputSchema = z.object({
70
+ id: z.string().min(1).optional(),
71
+ profile: z.enum(['logistics', 'healthcare', 'loyalty']).default('logistics'),
72
+ programName: z.string(),
73
+ site: z.string().optional(),
74
+ window: TimeWindowSchema.optional(),
75
+ capacity: z.number().positive().optional(),
76
+ metadata: z.record(z.any()).optional()
77
+ })
78
+
79
+ export type CreateParentInput = z.infer<typeof CreateParentInputSchema>
80
+
81
+ // Create child input schema
82
+ export const CreateChildInputSchema = z.object({
83
+ id: z.string().min(1).optional(),
84
+ profile: z.enum(['logistics', 'healthcare', 'loyalty']).default('logistics'),
85
+ parentId: z.string(),
86
+ // Logistics fields
87
+ plate: z.string().optional(),
88
+ carrier: z.string().optional(),
89
+ client: z.string().optional(),
90
+ // Healthcare fields
91
+ patientName: z.string().optional(),
92
+ procedure: z.string().optional(),
93
+ doctor: z.string().optional(),
94
+
95
+ // Loyalty fields
96
+ businessId: z.string().optional(),
97
+ customerId: z.string().optional(),
98
+ customerName: z.string().optional(),
99
+ memberId: z.string().optional(),
100
+ points: z.number().nonnegative().optional(),
101
+ metadata: z.record(z.any()).optional()
102
+ })
103
+
104
+ // Loyalty domain (multi-tenant) types
105
+ export interface LoyaltyBusiness {
106
+ id: string
107
+ name: string
108
+ programName: string
109
+ pointsLabel: string
110
+ loyaltyProgramId?: string
111
+ createdAt: string
112
+ updatedAt: string
113
+ }
114
+
115
+ export interface LoyaltyCustomerAccount {
116
+ id: string
117
+ businessId: string
118
+ fullName: string
119
+ memberId: string
120
+ createdAt: string
121
+ updatedAt: string
122
+ }
123
+
124
+ export const CreateBusinessInputSchema = z.object({
125
+ id: z.string().min(1).optional(),
126
+ name: z.string().min(1),
127
+ programName: z.string().min(1).optional(),
128
+ pointsLabel: z.string().min(1).optional()
129
+ })
130
+
131
+ export type CreateBusinessInput = z.infer<typeof CreateBusinessInputSchema>
132
+
133
+ export const CreateCustomerAccountInputSchema = z.object({
134
+ id: z.string().min(1).optional(),
135
+ businessId: z.string().min(1),
136
+ fullName: z.string().min(1),
137
+ memberId: z.string().min(1).optional()
138
+ })
139
+
140
+ export type CreateCustomerAccountInput = z.infer<typeof CreateCustomerAccountInputSchema>
141
+
142
+ export const CreateLoyaltyProgramInputSchema = z.object({
143
+ programId: z.string().min(1).optional(),
144
+ businessId: z.string().min(1),
145
+ // Optional overrides
146
+ programName: z.string().min(1).optional(),
147
+ site: z.string().optional(),
148
+ // Google Wallet geo-fence locations (latitude/longitude pairs)
149
+ locations: z.array(z.object({
150
+ latitude: z.number().min(-90).max(90),
151
+ longitude: z.number().min(-180).max(180)
152
+ })).optional(),
153
+ countryCode: z.string().length(2).optional(),
154
+ homepageUrl: z.string().url().optional(),
155
+ metadata: z.record(z.any()).optional()
156
+ })
157
+
158
+ export type CreateLoyaltyProgramInput = z.infer<typeof CreateLoyaltyProgramInputSchema>
159
+
160
+ export const IssueLoyaltyCardInputSchema = z.object({
161
+ cardId: z.string().min(1).optional(),
162
+ businessId: z.string().min(1),
163
+ customerId: z.string().min(1),
164
+ initialPoints: z.number().nonnegative().optional().default(0),
165
+ metadata: z.record(z.any()).optional()
166
+ })
167
+
168
+ export type IssueLoyaltyCardInput = z.infer<typeof IssueLoyaltyCardInputSchema>
169
+
170
+ export const UpdateLoyaltyPointsInputSchema = z.object({
171
+ cardId: z.string().min(1),
172
+ // Use either setPoints or delta
173
+ setPoints: z.number().nonnegative().optional(),
174
+ delta: z.number().int().optional()
175
+ }).refine(v => v.setPoints !== undefined || v.delta !== undefined, {
176
+ message: 'Provide either setPoints or delta'
177
+ })
178
+
179
+ export type UpdateLoyaltyPointsInput = z.infer<typeof UpdateLoyaltyPointsInputSchema>
180
+
181
+ export type GeoLocation = {
182
+ latitude: number
183
+ longitude: number
184
+ }
185
+
186
+ export type CreateChildInput = z.infer<typeof CreateChildInputSchema>
187
+
188
+ // Apple Wallet specific types
189
+ export interface ApplePassConfig {
190
+ teamId: string
191
+ passTypeId: string
192
+ certPath: string
193
+ certPassword: string
194
+ wwdrPath: string
195
+ }
196
+
197
+ export interface ApplePassField {
198
+ key: string
199
+ label: string
200
+ value: string | number
201
+ textAlignment?: 'PKTextAlignmentLeft' | 'PKTextAlignmentCenter' | 'PKTextAlignmentRight' | 'PKTextAlignmentNatural'
202
+ }
203
+
204
+ export interface ApplePassTemplate {
205
+ formatVersion: number
206
+ passTypeIdentifier: string
207
+ serialNumber: string
208
+ teamIdentifier: string
209
+ organizationName: string
210
+ description: string
211
+ backgroundColor?: string
212
+ foregroundColor?: string
213
+ labelColor?: string
214
+ logoText?: string
215
+ generic?: {
216
+ primaryFields?: ApplePassField[]
217
+ secondaryFields?: ApplePassField[]
218
+ auxiliaryFields?: ApplePassField[]
219
+ backFields?: ApplePassField[]
220
+ headerFields?: ApplePassField[]
221
+ }
222
+ barcode?: {
223
+ message: string
224
+ format: string
225
+ messageEncoding: string
226
+ }
227
+ barcodes?: Array<{
228
+ message: string
229
+ format: string
230
+ messageEncoding: string
231
+ }>
232
+ }
233
+
234
+ // Google Wallet specific types
235
+ export interface GooglePassConfig {
236
+ issuerId: string
237
+ serviceAccountPath?: string
238
+ }
239
+
240
+ export interface GoogleTextField {
241
+ header?: string
242
+ body?: string
243
+ }
244
+
245
+ export interface GooglePassClass {
246
+ id: string
247
+ issuerName: string
248
+ reviewStatus?: string
249
+ }
250
+
251
+ export interface GooglePassObject {
252
+ id: string
253
+ classId: string
254
+ state?: string
255
+ locations?: GeoLocation[]
256
+ linksModuleData?: {
257
+ uris: Array<{
258
+ uri: string
259
+ description?: string
260
+ id?: string
261
+ }>
262
+ }
263
+ imageModulesData?: Array<{
264
+ mainImage: {
265
+ sourceUri: {
266
+ uri: string
267
+ }
268
+ }
269
+ }>
270
+ messages?: Array<{
271
+ id: string
272
+ header: string
273
+ body: string
274
+ messageType?: string
275
+ }>
276
+ barcode?: {
277
+ type: string
278
+ value: string
279
+ }
280
+ cardTitle?: GoogleTextField
281
+ header?: GoogleTextField
282
+ textModulesData?: Array<{
283
+ header: string
284
+ body: string
285
+ id: string
286
+ }>
287
+ heroImage?: {
288
+ sourceUri: {
289
+ uri: string
290
+ }
291
+ }
292
+ hexBackgroundColor?: string
293
+ }
294
+
295
+ // Profile configuration
296
+ export interface ProfileFieldMap {
297
+ parent: Record<string, { label: string; key: string }>
298
+ child: Record<string, { label: string; key: string }>
299
+ }
300
+
301
+ export interface ProfileConfig {
302
+ name: ProfileType
303
+ fieldMap: ProfileFieldMap
304
+ statusFlow: PassStatus[]
305
+ defaultTemplates: {
306
+ apple: {
307
+ parent: Partial<ApplePassTemplate>
308
+ child: Partial<ApplePassTemplate>
309
+ }
310
+ google: {
311
+ parentClass: Partial<GooglePassClass>
312
+ parentObject: Partial<GooglePassObject>
313
+ childObject: Partial<GooglePassObject>
314
+ }
315
+ }
316
+ }
317
+
318
+ // Pass generation result
319
+ export interface PassGenerationResult {
320
+ passData: PassData
321
+ applePkpass?: Buffer
322
+ googleSaveUrl?: string
323
+ googleObject?: GooglePassObject
324
+ }
@@ -0,0 +1,130 @@
1
+ import { createCanvas } from 'canvas'
2
+
3
+ export type LogisticsStatus = 'ISSUED' | 'PRESENCE' | 'SCALE' | 'OPS' | 'EXITED'
4
+ export type HealthcareStatus = 'SCHEDULED' | 'CHECKIN' | 'PROCEDURE' | 'DISCHARGED'
5
+
6
+ const LOGISTICS_STEPS = ['ISSUED', 'PRESENCE', 'SCALE', 'OPS', 'EXITED']
7
+ const HEALTHCARE_STEPS = ['SCHEDULED', 'CHECKIN', 'PROCEDURE', 'DISCHARGED']
8
+
9
+ const STATUS_COLORS = {
10
+ ISSUED: { bg: '#4A90E2', text: '#FFFFFF', bar: '#2E5C8A' },
11
+ PRESENCE: { bg: '#F5A623', text: '#FFFFFF', bar: '#C47F1A' },
12
+ SCALE: { bg: '#7B68EE', text: '#FFFFFF', bar: '#5B4BB8' },
13
+ OPS: { bg: '#50E3C2', text: '#1A1A1A', bar: '#3AB89E' },
14
+ EXITED: { bg: '#7ED321', text: '#FFFFFF', bar: '#5FA519' },
15
+ SCHEDULED: { bg: '#4A90E2', text: '#FFFFFF', bar: '#2E5C8A' },
16
+ CHECKIN: { bg: '#F5A623', text: '#FFFFFF', bar: '#C47F1A' },
17
+ PROCEDURE: { bg: '#E94B3C', text: '#FFFFFF', bar: '#B93A2E' },
18
+ DISCHARGED: { bg: '#7ED321', text: '#FFFFFF', bar: '#5FA519' }
19
+ }
20
+
21
+ interface ProgressImageOptions {
22
+ width?: number
23
+ height?: number
24
+ status: LogisticsStatus | HealthcareStatus
25
+ steps: string[]
26
+ title?: string
27
+ }
28
+
29
+ /**
30
+ * Generates a progress bar image for Google Wallet hero image
31
+ * Dimensions: 1032ร—336 px (3:1 ratio) as recommended by Google
32
+ */
33
+ export async function generateProgressImage(options: ProgressImageOptions): Promise<Buffer> {
34
+ const {
35
+ width = 1032,
36
+ height = 336,
37
+ status,
38
+ steps,
39
+ title = 'Progress'
40
+ } = options
41
+
42
+ const canvas = createCanvas(width, height)
43
+ const ctx = canvas.getContext('2d')
44
+
45
+ const colors = STATUS_COLORS[status as keyof typeof STATUS_COLORS]
46
+ const currentIndex = steps.indexOf(status)
47
+ const progress = (currentIndex + 1) / steps.length
48
+
49
+ // Background gradient
50
+ const gradient = ctx.createLinearGradient(0, 0, 0, height)
51
+ gradient.addColorStop(0, colors.bg)
52
+ gradient.addColorStop(1, colors.bar)
53
+ ctx.fillStyle = gradient
54
+ ctx.fillRect(0, 0, width, height)
55
+
56
+ // Title
57
+ ctx.fillStyle = colors.text
58
+ ctx.font = 'bold 48px Arial, sans-serif'
59
+ ctx.textAlign = 'center'
60
+ ctx.fillText(title, width / 2, 70)
61
+
62
+ // Current status
63
+ ctx.font = 'bold 64px Arial, sans-serif'
64
+ ctx.fillText(status, width / 2, 150)
65
+
66
+ // Progress bar background
67
+ const barWidth = width * 0.8
68
+ const barHeight = 40
69
+ const barX = (width - barWidth) / 2
70
+ const barY = height - 120
71
+
72
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
73
+ ctx.roundRect(barX, barY, barWidth, barHeight, 20)
74
+ ctx.fill()
75
+
76
+ // Progress bar fill
77
+ ctx.fillStyle = colors.text
78
+ ctx.roundRect(barX, barY, barWidth * progress, barHeight, 20)
79
+ ctx.fill()
80
+
81
+ // Step indicators
82
+ const stepWidth = barWidth / steps.length
83
+ ctx.font = 'bold 20px Arial, sans-serif'
84
+ ctx.textAlign = 'center'
85
+
86
+ steps.forEach((step, index) => {
87
+ const x = barX + stepWidth * index + stepWidth / 2
88
+ const y = barY - 20
89
+
90
+ // Step dot
91
+ ctx.beginPath()
92
+ ctx.arc(x, barY + barHeight / 2, 12, 0, Math.PI * 2)
93
+ ctx.fillStyle = index <= currentIndex ? colors.text : 'rgba(255, 255, 255, 0.5)'
94
+ ctx.fill()
95
+
96
+ // Step label
97
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'
98
+ ctx.fillText(step, x, y)
99
+ })
100
+
101
+ // Progress percentage
102
+ ctx.font = 'bold 28px Arial, sans-serif'
103
+ ctx.fillStyle = colors.text
104
+ ctx.textAlign = 'center'
105
+ ctx.fillText(`${Math.round(progress * 100)}% Complete`, width / 2, height - 40)
106
+
107
+ return canvas.toBuffer('image/png')
108
+ }
109
+
110
+ /**
111
+ * Generate hero image for logistics passes
112
+ */
113
+ export async function generateLogisticsHeroImage(status: LogisticsStatus): Promise<Buffer> {
114
+ return generateProgressImage({
115
+ status,
116
+ steps: LOGISTICS_STEPS,
117
+ title: 'Transport Order Progress'
118
+ })
119
+ }
120
+
121
+ /**
122
+ * Generate hero image for healthcare passes
123
+ */
124
+ export async function generateHealthcareHeroImage(status: HealthcareStatus): Promise<Buffer> {
125
+ return generateProgressImage({
126
+ status,
127
+ steps: HEALTHCARE_STEPS,
128
+ title: 'Patient Visit Progress'
129
+ })
130
+ }
@@ -0,0 +1,78 @@
1
+ import { createParentSchedule, createChildTicket, getGoogleObject } from './dist/index.js';
2
+ import 'dotenv/config';
3
+ async function testGoogleWallet() {
4
+ console.log('๐Ÿงช Testing Google Wallet Pass Generation\n');
5
+ // Verify environment variables
6
+ if (!process.env.GOOGLE_ISSUER_ID) {
7
+ console.error('โŒ GOOGLE_ISSUER_ID not set in .env file');
8
+ console.log('\nPlease add to .env:');
9
+ console.log('GOOGLE_ISSUER_ID=your_issuer_id_here');
10
+ process.exit(1);
11
+ }
12
+ if (!process.env.GOOGLE_SA_JSON) {
13
+ console.error('โŒ GOOGLE_SA_JSON not set in .env file');
14
+ console.log('\nReal device testing requires a Google service account JSON key:');
15
+ console.log('GOOGLE_SA_JSON=./certs/google-credentials.json');
16
+ console.log('\nWithout this, the Save URL will be unsigned and Google Wallet will not accept it.');
17
+ process.exit(1);
18
+ }
19
+ console.log('โœ… Issuer ID found:', process.env.GOOGLE_ISSUER_ID);
20
+ // Create parent
21
+ const parent = await createParentSchedule({
22
+ profile: 'logistics',
23
+ programName: 'Test Yard Veracruz',
24
+ site: 'Patio Gate 3',
25
+ window: {
26
+ from: '2025-10-20T08:00:00-06:00',
27
+ to: '2025-10-20T12:00:00-06:00',
28
+ tz: 'America/Mexico_City'
29
+ }
30
+ });
31
+ console.log('\nโœ… Parent created:', parent.id);
32
+ console.log(' Program:', parent.programName);
33
+ console.log(' Site:', parent.site);
34
+ // Create child
35
+ const child = await createChildTicket({
36
+ profile: 'logistics',
37
+ parentId: parent.id,
38
+ plate: 'TEST123',
39
+ carrier: 'Test Transport',
40
+ client: 'Test Client'
41
+ });
42
+ console.log('\nโœ… Child created:', child.id);
43
+ console.log(' Plate:', child.plate);
44
+ console.log(' Carrier:', child.carrier);
45
+ // Generate Google Wallet object
46
+ try {
47
+ const { object, saveUrl } = await getGoogleObject('child', child);
48
+ console.log('\n' + '='.repeat(60));
49
+ console.log('โœ… GOOGLE WALLET OBJECT GENERATED!');
50
+ console.log('='.repeat(60));
51
+ console.log('\n๐Ÿ“‹ Object Details:');
52
+ console.log(JSON.stringify(object, null, 2));
53
+ console.log('\n' + '='.repeat(60));
54
+ console.log('๐Ÿ”— SAVE TO WALLET URL:');
55
+ console.log('='.repeat(60));
56
+ console.log(saveUrl);
57
+ const looksSigned = saveUrl.includes('/eyJ');
58
+ if (!looksSigned) {
59
+ console.log('\nโš ๏ธ Save URL does not look like a signed JWT.');
60
+ console.log(' Ensure GOOGLE_SA_JSON points to a valid service account JSON file.');
61
+ }
62
+ console.log('\n' + '='.repeat(60));
63
+ console.log('๐Ÿ“ฑ TO TEST ON YOUR DEVICE:');
64
+ console.log('='.repeat(60));
65
+ console.log('1. Copy the Save URL above');
66
+ console.log('2. Open it on your Android phone or any browser');
67
+ console.log('3. Click "Save to Google Wallet"');
68
+ console.log('4. Pass will appear in your Google Wallet app!');
69
+ console.log('\n' + '='.repeat(60));
70
+ console.log('๐ŸŽ‰ SUCCESS!');
71
+ console.log('='.repeat(60));
72
+ }
73
+ catch (error) {
74
+ console.error('\nโŒ Error generating pass:', error);
75
+ throw error;
76
+ }
77
+ }
78
+ testGoogleWallet().catch(console.error);
@@ -0,0 +1,94 @@
1
+ import { createParentSchedule, createChildTicket, getGoogleObject } from './dist/index.js'
2
+ import 'dotenv/config'
3
+
4
+ async function testGoogleWallet() {
5
+ console.log('๐Ÿงช Testing Google Wallet Pass Generation\n')
6
+
7
+ // Verify environment variables
8
+ if (!process.env.GOOGLE_ISSUER_ID) {
9
+ console.error('โŒ GOOGLE_ISSUER_ID not set in .env file')
10
+ console.log('\nPlease add to .env:')
11
+ console.log('GOOGLE_ISSUER_ID=your_issuer_id_here')
12
+ process.exit(1)
13
+ }
14
+
15
+ if (!process.env.GOOGLE_SA_JSON) {
16
+ console.error('โŒ GOOGLE_SA_JSON not set in .env file')
17
+ console.log('\nReal device testing requires a Google service account JSON key:')
18
+ console.log('GOOGLE_SA_JSON=./certs/google-credentials.json')
19
+ console.log('\nWithout this, the Save URL will be unsigned and Google Wallet will not accept it.')
20
+ process.exit(1)
21
+ }
22
+
23
+ console.log('โœ… Issuer ID found:', process.env.GOOGLE_ISSUER_ID)
24
+
25
+ // Create parent
26
+ const parent = await createParentSchedule({
27
+ profile: 'logistics',
28
+ programName: 'Test Yard Veracruz',
29
+ site: 'Patio Gate 3',
30
+ window: {
31
+ from: '2025-10-20T08:00:00-06:00',
32
+ to: '2025-10-20T12:00:00-06:00',
33
+ tz: 'America/Mexico_City'
34
+ }
35
+ })
36
+
37
+ console.log('\nโœ… Parent created:', parent.id)
38
+ console.log(' Program:', parent.programName)
39
+ console.log(' Site:', parent.site)
40
+
41
+ // Create child
42
+ const child = await createChildTicket({
43
+ profile: 'logistics',
44
+ parentId: parent.id,
45
+ plate: 'TEST123',
46
+ carrier: 'Test Transport',
47
+ client: 'Test Client'
48
+ })
49
+
50
+ console.log('\nโœ… Child created:', child.id)
51
+ console.log(' Plate:', child.plate)
52
+ console.log(' Carrier:', child.carrier)
53
+
54
+ // Generate Google Wallet object
55
+ try {
56
+ const { object, saveUrl } = await getGoogleObject('child', child)
57
+
58
+ console.log('\n' + '='.repeat(60))
59
+ console.log('โœ… GOOGLE WALLET OBJECT GENERATED!')
60
+ console.log('='.repeat(60))
61
+
62
+ console.log('\n๐Ÿ“‹ Object Details:')
63
+ console.log(JSON.stringify(object, null, 2))
64
+
65
+ console.log('\n' + '='.repeat(60))
66
+ console.log('๐Ÿ”— SAVE TO WALLET URL:')
67
+ console.log('='.repeat(60))
68
+ console.log(saveUrl)
69
+
70
+ const looksSigned = saveUrl.includes('/eyJ')
71
+ if (!looksSigned) {
72
+ console.log('\nโš ๏ธ Save URL does not look like a signed JWT.')
73
+ console.log(' Ensure GOOGLE_SA_JSON points to a valid service account JSON file.')
74
+ }
75
+
76
+ console.log('\n' + '='.repeat(60))
77
+ console.log('๐Ÿ“ฑ TO TEST ON YOUR DEVICE:')
78
+ console.log('='.repeat(60))
79
+ console.log('1. Copy the Save URL above')
80
+ console.log('2. Open it on your Android phone or any browser')
81
+ console.log('3. Click "Save to Google Wallet"')
82
+ console.log('4. Pass will appear in your Google Wallet app!')
83
+
84
+ console.log('\n' + '='.repeat(60))
85
+ console.log('๐ŸŽ‰ SUCCESS!')
86
+ console.log('='.repeat(60))
87
+
88
+ } catch (error) {
89
+ console.error('\nโŒ Error generating pass:', error)
90
+ throw error
91
+ }
92
+ }
93
+
94
+ testGoogleWallet().catch(console.error)