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.
- package/.env.example +10 -0
- package/.github/workflows/build.yml +66 -0
- package/.github/workflows/release.yml +57 -0
- package/APPLE_WALLET_SETUP.md +318 -0
- package/GOOGLE_WALLET_SETUP.md +473 -0
- package/LICENSE +201 -0
- package/README.md +187 -0
- package/dist/adapters/apple.d.ts +10 -0
- package/dist/adapters/apple.js +153 -0
- package/dist/adapters/google.d.ts +26 -0
- package/dist/adapters/google.js +431 -0
- package/dist/api/unified.d.ts +67 -0
- package/dist/api/unified.js +375 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -0
- package/dist/profiles/healthcare/index.d.ts +91 -0
- package/dist/profiles/healthcare/index.js +151 -0
- package/dist/profiles/logistics/index.d.ts +91 -0
- package/dist/profiles/logistics/index.js +152 -0
- package/dist/profiles/loyalty/index.d.ts +91 -0
- package/dist/profiles/loyalty/index.js +81 -0
- package/dist/templates/apple/child.json +59 -0
- package/dist/templates/apple/parent.json +54 -0
- package/dist/templates/google/child_object.json +38 -0
- package/dist/templates/google/loyalty_class.json +7 -0
- package/dist/templates/google/loyalty_object.json +29 -0
- package/dist/templates/google/parent_class.json +10 -0
- package/dist/templates/google/parent_object.json +33 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.js +80 -0
- package/dist/utils/progress-image.d.ts +23 -0
- package/dist/utils/progress-image.js +94 -0
- package/examples/.loyalty-fixed-state.json +10 -0
- package/examples/claim-flow.ts +163 -0
- package/examples/loyalty-admin-server.js +207 -0
- package/examples/loyalty-admin.html +260 -0
- package/examples/loyalty-fixed-card-server.js +288 -0
- package/examples/loyalty-flow.ts +78 -0
- package/examples/loyalty-google-issue.js +115 -0
- package/package.json +51 -0
- package/scripts/copy-assets.js +35 -0
- package/scripts/smoke-dist-import.js +39 -0
- package/setup-google-class.js +97 -0
- package/setup-google-class.ts +105 -0
- package/src/adapters/apple.ts +193 -0
- package/src/adapters/google.ts +521 -0
- package/src/api/unified.ts +487 -0
- package/src/index.ts +74 -0
- package/src/profiles/healthcare/index.ts +157 -0
- package/src/profiles/logistics/index.ts +158 -0
- package/src/profiles/loyalty/index.ts +87 -0
- package/src/templates/apple/child.json +59 -0
- package/src/templates/apple/parent.json +54 -0
- package/src/templates/google/child_object.json +38 -0
- package/src/templates/google/loyalty_class.json +7 -0
- package/src/templates/google/loyalty_object.json +29 -0
- package/src/templates/google/parent_class.json +10 -0
- package/src/templates/google/parent_object.json +33 -0
- package/src/types.ts +324 -0
- package/src/utils/progress-image.ts +130 -0
- package/test-google-wallet.js +78 -0
- package/test-google-wallet.ts +94 -0
- package/tests/adapters.test.ts +244 -0
- package/tests/loyalty.test.ts +39 -0
- package/tests/unified.test.ts +388 -0
- package/tsconfig.json +19 -0
- 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)
|