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
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateParentInput,
|
|
3
|
+
CreateChildInput,
|
|
4
|
+
CreateBusinessInput,
|
|
5
|
+
CreateCustomerAccountInput,
|
|
6
|
+
CreateLoyaltyProgramInput,
|
|
7
|
+
IssueLoyaltyCardInput,
|
|
8
|
+
UpdateLoyaltyPointsInput,
|
|
9
|
+
LoyaltyBusiness,
|
|
10
|
+
LoyaltyCustomerAccount,
|
|
11
|
+
ParentPassData,
|
|
12
|
+
ChildPassData,
|
|
13
|
+
PassData,
|
|
14
|
+
PassStatus,
|
|
15
|
+
ProfileType,
|
|
16
|
+
ProfileConfig,
|
|
17
|
+
PassGenerationResult
|
|
18
|
+
} from '../types.js'
|
|
19
|
+
import {
|
|
20
|
+
CreateParentInputSchema,
|
|
21
|
+
CreateChildInputSchema,
|
|
22
|
+
CreateBusinessInputSchema,
|
|
23
|
+
CreateCustomerAccountInputSchema,
|
|
24
|
+
CreateLoyaltyProgramInputSchema,
|
|
25
|
+
IssueLoyaltyCardInputSchema,
|
|
26
|
+
UpdateLoyaltyPointsInputSchema
|
|
27
|
+
} from '../types.js'
|
|
28
|
+
import { AppleWalletAdapter } from '../adapters/apple.js'
|
|
29
|
+
import { GoogleWalletAdapter } from '../adapters/google.js'
|
|
30
|
+
import logisticsProfile from '../profiles/logistics/index.js'
|
|
31
|
+
import healthcareProfile from '../profiles/healthcare/index.js'
|
|
32
|
+
import loyaltyProfile from '../profiles/loyalty/index.js'
|
|
33
|
+
|
|
34
|
+
const hashEvent = (data: any): string => {
|
|
35
|
+
// Include timestamp and random value to ensure unique hashes
|
|
36
|
+
const str = JSON.stringify({ ...data, _timestamp: Date.now(), _random: Math.random() })
|
|
37
|
+
return `hash_${Buffer.from(str).toString('base64').slice(0, 32)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const signCredential = (hash: string): string => {
|
|
41
|
+
return `sig_${hash.slice(5, 37)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// In-memory storage (replace with actual database in production)
|
|
45
|
+
const passStore = new Map<string, PassData>()
|
|
46
|
+
const businessStore = new Map<string, LoyaltyBusiness>()
|
|
47
|
+
const customerStore = new Map<string, LoyaltyCustomerAccount>()
|
|
48
|
+
|
|
49
|
+
// Profile registry
|
|
50
|
+
const profiles: Record<ProfileType, ProfileConfig> = {
|
|
51
|
+
logistics: logisticsProfile,
|
|
52
|
+
healthcare: healthcareProfile,
|
|
53
|
+
loyalty: loyaltyProfile
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get a profile by name
|
|
58
|
+
*/
|
|
59
|
+
export function getProfile(profileType: ProfileType): ProfileConfig {
|
|
60
|
+
return profiles[profileType]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all available profiles
|
|
65
|
+
*/
|
|
66
|
+
export function listProfiles(): ProfileType[] {
|
|
67
|
+
return Object.keys(profiles) as ProfileType[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a unique ID for a pass
|
|
72
|
+
*/
|
|
73
|
+
function generatePassId(profile: ProfileType, type: 'parent' | 'child', parentId?: string): string {
|
|
74
|
+
const date = new Date().toISOString().split('T')[0]
|
|
75
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase()
|
|
76
|
+
|
|
77
|
+
if (type === 'parent') {
|
|
78
|
+
const prefix = profile === 'logistics' ? 'PES' : profile === 'healthcare' ? 'APB' : 'LPR'
|
|
79
|
+
return `${prefix}-${date}-${random}`
|
|
80
|
+
} else {
|
|
81
|
+
const prefix = profile === 'logistics' ? 'TO' : profile === 'healthcare' ? 'PV' : 'LCR'
|
|
82
|
+
const parentSuffix = parentId ? parentId.split('-').pop() : random
|
|
83
|
+
return `${prefix}-${date}-${parentSuffix}-${random}`
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateBusinessId(): string {
|
|
88
|
+
const date = new Date().toISOString().split('T')[0]
|
|
89
|
+
const random = Math.random().toString(36).substring(2, 8).toUpperCase()
|
|
90
|
+
return `BIZ-${date}-${random}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateCustomerId(): string {
|
|
94
|
+
const date = new Date().toISOString().split('T')[0]
|
|
95
|
+
const random = Math.random().toString(36).substring(2, 8).toUpperCase()
|
|
96
|
+
return `CUS-${date}-${random}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function generateMemberId(businessId: string): string {
|
|
100
|
+
const bizSuffix = businessId.split('-').pop() || 'BIZ'
|
|
101
|
+
const random = Math.random().toString(36).substring(2, 10).toUpperCase()
|
|
102
|
+
// This is the value we encode into the QR code / barcode.
|
|
103
|
+
return `SBC-${bizSuffix}-${random}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a business (tenant) that owns a loyalty program.
|
|
108
|
+
*/
|
|
109
|
+
export function createBusiness(input: CreateBusinessInput): LoyaltyBusiness {
|
|
110
|
+
const validated = CreateBusinessInputSchema.parse(input)
|
|
111
|
+
|
|
112
|
+
const id = (validated as any).id || generateBusinessId()
|
|
113
|
+
const now = new Date().toISOString()
|
|
114
|
+
|
|
115
|
+
const business: LoyaltyBusiness = {
|
|
116
|
+
id,
|
|
117
|
+
name: validated.name,
|
|
118
|
+
programName: validated.programName || `${validated.name} Loyalty`,
|
|
119
|
+
pointsLabel: validated.pointsLabel || 'Points',
|
|
120
|
+
createdAt: now,
|
|
121
|
+
updatedAt: now
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
businessStore.set(id, business)
|
|
125
|
+
return business
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getBusiness(businessId: string): LoyaltyBusiness | undefined {
|
|
129
|
+
return businessStore.get(businessId)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a customer account under a business.
|
|
134
|
+
*/
|
|
135
|
+
export function createCustomerAccount(input: CreateCustomerAccountInput): LoyaltyCustomerAccount {
|
|
136
|
+
const validated = CreateCustomerAccountInputSchema.parse(input)
|
|
137
|
+
|
|
138
|
+
const business = businessStore.get(validated.businessId)
|
|
139
|
+
if (!business) {
|
|
140
|
+
throw new Error(`Business not found: ${validated.businessId}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const id = (validated as any).id || generateCustomerId()
|
|
144
|
+
const now = new Date().toISOString()
|
|
145
|
+
|
|
146
|
+
const customer: LoyaltyCustomerAccount = {
|
|
147
|
+
id,
|
|
148
|
+
businessId: validated.businessId,
|
|
149
|
+
fullName: validated.fullName,
|
|
150
|
+
memberId: (validated as any).memberId || generateMemberId(validated.businessId),
|
|
151
|
+
createdAt: now,
|
|
152
|
+
updatedAt: now
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
customerStore.set(id, customer)
|
|
156
|
+
return customer
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getCustomerAccount(customerId: string): LoyaltyCustomerAccount | undefined {
|
|
160
|
+
return customerStore.get(customerId)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Define (or update) the loyalty program pass for a business.
|
|
165
|
+
* This creates a parent pass with profile=loyalty.
|
|
166
|
+
*/
|
|
167
|
+
export async function createLoyaltyProgram(input: CreateLoyaltyProgramInput): Promise<ParentPassData> {
|
|
168
|
+
const validated = CreateLoyaltyProgramInputSchema.parse(input)
|
|
169
|
+
|
|
170
|
+
const business = businessStore.get(validated.businessId)
|
|
171
|
+
if (!business) {
|
|
172
|
+
throw new Error(`Business not found: ${validated.businessId}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const program = await createParentSchedule({
|
|
176
|
+
id: (validated as any).programId,
|
|
177
|
+
profile: 'loyalty',
|
|
178
|
+
programName: validated.programName || business.programName,
|
|
179
|
+
site: validated.site,
|
|
180
|
+
metadata: {
|
|
181
|
+
...validated.metadata,
|
|
182
|
+
businessId: business.id,
|
|
183
|
+
businessName: business.name,
|
|
184
|
+
pointsLabel: business.pointsLabel,
|
|
185
|
+
googleWallet: {
|
|
186
|
+
...(validated.metadata as any)?.googleWallet,
|
|
187
|
+
locations: validated.locations,
|
|
188
|
+
countryCode: validated.countryCode,
|
|
189
|
+
homepageUrl: validated.homepageUrl
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
business.loyaltyProgramId = program.id
|
|
195
|
+
business.updatedAt = new Date().toISOString()
|
|
196
|
+
businessStore.set(business.id, business)
|
|
197
|
+
|
|
198
|
+
return program
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Issue a loyalty card (child pass) for a customer.
|
|
203
|
+
* QR/barcode value uses memberId.
|
|
204
|
+
*/
|
|
205
|
+
export async function issueLoyaltyCard(input: IssueLoyaltyCardInput): Promise<ChildPassData> {
|
|
206
|
+
const validated = IssueLoyaltyCardInputSchema.parse(input)
|
|
207
|
+
|
|
208
|
+
const business = businessStore.get(validated.businessId)
|
|
209
|
+
if (!business) {
|
|
210
|
+
throw new Error(`Business not found: ${validated.businessId}`)
|
|
211
|
+
}
|
|
212
|
+
if (!business.loyaltyProgramId) {
|
|
213
|
+
throw new Error(`Business has no loyalty program yet: ${validated.businessId}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const customer = customerStore.get(validated.customerId)
|
|
217
|
+
if (!customer || customer.businessId !== validated.businessId) {
|
|
218
|
+
throw new Error(`Customer not found for business: ${validated.customerId}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const program = passStore.get(business.loyaltyProgramId)
|
|
222
|
+
const programGoogleWallet = program && program.type === 'parent' ? (program.metadata as any)?.googleWallet : undefined
|
|
223
|
+
|
|
224
|
+
const card = await createChildTicket({
|
|
225
|
+
id: (validated as any).cardId,
|
|
226
|
+
profile: 'loyalty',
|
|
227
|
+
parentId: business.loyaltyProgramId,
|
|
228
|
+
businessId: business.id,
|
|
229
|
+
customerId: customer.id,
|
|
230
|
+
customerName: customer.fullName,
|
|
231
|
+
memberId: customer.memberId,
|
|
232
|
+
points: validated.initialPoints,
|
|
233
|
+
metadata: {
|
|
234
|
+
...validated.metadata,
|
|
235
|
+
businessName: business.name,
|
|
236
|
+
pointsLabel: business.pointsLabel,
|
|
237
|
+
googleWallet: {
|
|
238
|
+
...(programGoogleWallet || {}),
|
|
239
|
+
...((validated.metadata as any)?.googleWallet || {})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Loyalty cards start as ACTIVE unless explicitly overridden
|
|
245
|
+
card.status = 'ACTIVE' as PassStatus
|
|
246
|
+
card.updatedAt = new Date().toISOString()
|
|
247
|
+
card.hash = hashEvent(card)
|
|
248
|
+
card.signature = signCredential(card.hash)
|
|
249
|
+
passStore.set(card.id, card)
|
|
250
|
+
|
|
251
|
+
return card
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Update points on a loyalty card.
|
|
256
|
+
*/
|
|
257
|
+
export async function updateLoyaltyPoints(input: UpdateLoyaltyPointsInput): Promise<PassData> {
|
|
258
|
+
const validated = UpdateLoyaltyPointsInputSchema.parse(input)
|
|
259
|
+
const pass = passStore.get(validated.cardId)
|
|
260
|
+
|
|
261
|
+
if (!pass) {
|
|
262
|
+
throw new Error(`Pass not found: ${validated.cardId}`)
|
|
263
|
+
}
|
|
264
|
+
if (pass.type !== 'child' || pass.profile !== 'loyalty') {
|
|
265
|
+
throw new Error(`Not a loyalty card: ${validated.cardId}`)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const currentPoints = typeof pass.points === 'number' ? pass.points : 0
|
|
269
|
+
const nextPoints = validated.setPoints !== undefined
|
|
270
|
+
? validated.setPoints
|
|
271
|
+
: Math.max(0, currentPoints + (validated.delta || 0))
|
|
272
|
+
|
|
273
|
+
pass.points = nextPoints
|
|
274
|
+
pass.updatedAt = new Date().toISOString()
|
|
275
|
+
|
|
276
|
+
pass.hash = hashEvent(pass)
|
|
277
|
+
pass.signature = signCredential(pass.hash)
|
|
278
|
+
|
|
279
|
+
passStore.set(pass.id, pass)
|
|
280
|
+
return pass
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a parent schedule (PES or AppointmentBatch)
|
|
285
|
+
*/
|
|
286
|
+
export async function createParentSchedule(input: CreateParentInput): Promise<ParentPassData> {
|
|
287
|
+
// Validate input
|
|
288
|
+
const validated = CreateParentInputSchema.parse(input)
|
|
289
|
+
|
|
290
|
+
// Get profile
|
|
291
|
+
const profile = getProfile(validated.profile)
|
|
292
|
+
|
|
293
|
+
// Generate ID (allow callers to provide a stable one)
|
|
294
|
+
const id = (validated as any).id || generatePassId(validated.profile, 'parent')
|
|
295
|
+
|
|
296
|
+
// Get initial status
|
|
297
|
+
const initialStatus = profile.statusFlow[0] as PassStatus
|
|
298
|
+
|
|
299
|
+
// Create pass data
|
|
300
|
+
const passData: ParentPassData = {
|
|
301
|
+
id,
|
|
302
|
+
type: 'parent',
|
|
303
|
+
profile: validated.profile,
|
|
304
|
+
programName: validated.programName,
|
|
305
|
+
site: validated.site,
|
|
306
|
+
window: validated.window,
|
|
307
|
+
capacity: validated.capacity,
|
|
308
|
+
metadata: validated.metadata,
|
|
309
|
+
createdAt: new Date().toISOString(),
|
|
310
|
+
updatedAt: new Date().toISOString(),
|
|
311
|
+
status: initialStatus
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const hash = hashEvent(passData)
|
|
315
|
+
const signature = signCredential(hash)
|
|
316
|
+
|
|
317
|
+
passData.hash = hash
|
|
318
|
+
passData.signature = signature
|
|
319
|
+
|
|
320
|
+
// Store pass
|
|
321
|
+
passStore.set(id, passData)
|
|
322
|
+
|
|
323
|
+
return passData
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create a child ticket (TO or PatientVisit)
|
|
328
|
+
*/
|
|
329
|
+
export async function createChildTicket(input: CreateChildInput): Promise<ChildPassData> {
|
|
330
|
+
// Validate input
|
|
331
|
+
const validated = CreateChildInputSchema.parse(input)
|
|
332
|
+
|
|
333
|
+
// Verify parent exists
|
|
334
|
+
const parent = passStore.get(validated.parentId)
|
|
335
|
+
if (!parent || parent.type !== 'parent') {
|
|
336
|
+
throw new Error(`Parent pass not found: ${validated.parentId}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Get profile
|
|
340
|
+
const profile = getProfile(validated.profile)
|
|
341
|
+
|
|
342
|
+
// Generate ID (allow callers to provide a stable one)
|
|
343
|
+
const id = (validated as any).id || generatePassId(validated.profile, 'child', validated.parentId)
|
|
344
|
+
|
|
345
|
+
// Get initial status
|
|
346
|
+
const initialStatus = profile.statusFlow[0] as PassStatus
|
|
347
|
+
|
|
348
|
+
// Create pass data
|
|
349
|
+
const passData: ChildPassData = {
|
|
350
|
+
id,
|
|
351
|
+
type: 'child',
|
|
352
|
+
profile: validated.profile,
|
|
353
|
+
parentId: validated.parentId,
|
|
354
|
+
plate: validated.plate,
|
|
355
|
+
carrier: validated.carrier,
|
|
356
|
+
client: validated.client,
|
|
357
|
+
patientName: validated.patientName,
|
|
358
|
+
procedure: validated.procedure,
|
|
359
|
+
doctor: validated.doctor,
|
|
360
|
+
businessId: validated.businessId,
|
|
361
|
+
customerId: validated.customerId,
|
|
362
|
+
customerName: validated.customerName,
|
|
363
|
+
memberId: validated.memberId,
|
|
364
|
+
points: validated.points,
|
|
365
|
+
metadata: validated.metadata,
|
|
366
|
+
createdAt: new Date().toISOString(),
|
|
367
|
+
updatedAt: new Date().toISOString(),
|
|
368
|
+
status: initialStatus
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const hash = hashEvent(passData)
|
|
372
|
+
const signature = signCredential(hash)
|
|
373
|
+
|
|
374
|
+
passData.hash = hash
|
|
375
|
+
passData.signature = signature
|
|
376
|
+
|
|
377
|
+
// Store pass
|
|
378
|
+
passStore.set(id, passData)
|
|
379
|
+
|
|
380
|
+
return passData
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Update the status of a pass
|
|
385
|
+
*/
|
|
386
|
+
export async function updatePassStatus(passId: string, newStatus: PassStatus): Promise<PassData> {
|
|
387
|
+
// Get pass
|
|
388
|
+
const passData = passStore.get(passId)
|
|
389
|
+
if (!passData) {
|
|
390
|
+
throw new Error(`Pass not found: ${passId}`)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Get profile
|
|
394
|
+
const profile = getProfile(passData.profile)
|
|
395
|
+
|
|
396
|
+
// Validate status transition
|
|
397
|
+
if (!profile.statusFlow.includes(newStatus)) {
|
|
398
|
+
throw new Error(`Invalid status '${newStatus}' for profile '${passData.profile}'`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Update status
|
|
402
|
+
passData.status = newStatus
|
|
403
|
+
passData.updatedAt = new Date().toISOString()
|
|
404
|
+
|
|
405
|
+
// Re-hash and sign
|
|
406
|
+
const hash = hashEvent(passData)
|
|
407
|
+
const signature = signCredential(hash)
|
|
408
|
+
|
|
409
|
+
passData.hash = hash
|
|
410
|
+
passData.signature = signature
|
|
411
|
+
|
|
412
|
+
// Update store
|
|
413
|
+
passStore.set(passId, passData)
|
|
414
|
+
|
|
415
|
+
return passData
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get a pass by ID
|
|
420
|
+
*/
|
|
421
|
+
export function getPass(passId: string): PassData | undefined {
|
|
422
|
+
return passStore.get(passId)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get Apple Wallet .pkpass buffer for a pass
|
|
427
|
+
*/
|
|
428
|
+
export async function getPkpassBuffer(
|
|
429
|
+
passType: 'parent' | 'child',
|
|
430
|
+
passData: PassData
|
|
431
|
+
): Promise<Buffer> {
|
|
432
|
+
const profile = getProfile(passData.profile)
|
|
433
|
+
const adapter = new AppleWalletAdapter()
|
|
434
|
+
|
|
435
|
+
return adapter.generatePkpass(passData, profile, passType)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get Google Wallet object for a pass
|
|
440
|
+
*/
|
|
441
|
+
export async function getGoogleObject(
|
|
442
|
+
passType: 'parent' | 'child',
|
|
443
|
+
passData: PassData
|
|
444
|
+
): Promise<{ object: any; saveUrl: string }> {
|
|
445
|
+
const profile = getProfile(passData.profile)
|
|
446
|
+
const adapter = new GoogleWalletAdapter()
|
|
447
|
+
|
|
448
|
+
return adapter.generatePassObject(passData, profile, passType)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Generate a complete pass with both Apple and Google wallet data
|
|
453
|
+
*/
|
|
454
|
+
export async function generatePass(
|
|
455
|
+
passData: PassData,
|
|
456
|
+
options: {
|
|
457
|
+
includeApple?: boolean
|
|
458
|
+
includeGoogle?: boolean
|
|
459
|
+
} = { includeApple: true, includeGoogle: true }
|
|
460
|
+
): Promise<PassGenerationResult> {
|
|
461
|
+
const profile = getProfile(passData.profile)
|
|
462
|
+
const passType = passData.type
|
|
463
|
+
|
|
464
|
+
const result: PassGenerationResult = {
|
|
465
|
+
passData
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (options.includeApple) {
|
|
469
|
+
try {
|
|
470
|
+
result.applePkpass = await getPkpassBuffer(passType, passData)
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.warn('Failed to generate Apple Wallet pass:', error)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (options.includeGoogle) {
|
|
477
|
+
try {
|
|
478
|
+
const googleResult = await getGoogleObject(passType, passData)
|
|
479
|
+
result.googleObject = googleResult.object
|
|
480
|
+
result.googleSaveUrl = googleResult.saveUrl
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.warn('Failed to generate Google Wallet object:', error)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result
|
|
487
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export {
|
|
3
|
+
createParentSchedule,
|
|
4
|
+
createChildTicket,
|
|
5
|
+
updatePassStatus,
|
|
6
|
+
createBusiness,
|
|
7
|
+
getBusiness,
|
|
8
|
+
createCustomerAccount,
|
|
9
|
+
getCustomerAccount,
|
|
10
|
+
createLoyaltyProgram,
|
|
11
|
+
issueLoyaltyCard,
|
|
12
|
+
updateLoyaltyPoints,
|
|
13
|
+
getPkpassBuffer,
|
|
14
|
+
getGoogleObject,
|
|
15
|
+
listProfiles,
|
|
16
|
+
getProfile,
|
|
17
|
+
getPass,
|
|
18
|
+
generatePass
|
|
19
|
+
} from './api/unified.js'
|
|
20
|
+
|
|
21
|
+
// Adapter exports
|
|
22
|
+
export { AppleWalletAdapter } from './adapters/apple.js'
|
|
23
|
+
export { GoogleWalletAdapter } from './adapters/google.js'
|
|
24
|
+
|
|
25
|
+
// Profile exports
|
|
26
|
+
export { default as logisticsProfile } from './profiles/logistics/index.js'
|
|
27
|
+
export { default as healthcareProfile } from './profiles/healthcare/index.js'
|
|
28
|
+
export { default as loyaltyProfile } from './profiles/loyalty/index.js'
|
|
29
|
+
|
|
30
|
+
// Type exports
|
|
31
|
+
export type {
|
|
32
|
+
ProfileType,
|
|
33
|
+
PassStatus,
|
|
34
|
+
LogisticsStatus,
|
|
35
|
+
HealthcareStatus,
|
|
36
|
+
LoyaltyStatus,
|
|
37
|
+
GeoLocation,
|
|
38
|
+
TimeWindow,
|
|
39
|
+
BasePassData,
|
|
40
|
+
ParentPassData,
|
|
41
|
+
ChildPassData,
|
|
42
|
+
PassData,
|
|
43
|
+
CreateParentInput,
|
|
44
|
+
CreateChildInput,
|
|
45
|
+
LoyaltyBusiness,
|
|
46
|
+
LoyaltyCustomerAccount,
|
|
47
|
+
CreateBusinessInput,
|
|
48
|
+
CreateCustomerAccountInput,
|
|
49
|
+
CreateLoyaltyProgramInput,
|
|
50
|
+
IssueLoyaltyCardInput,
|
|
51
|
+
UpdateLoyaltyPointsInput,
|
|
52
|
+
ApplePassConfig,
|
|
53
|
+
ApplePassField,
|
|
54
|
+
ApplePassTemplate,
|
|
55
|
+
GooglePassConfig,
|
|
56
|
+
GoogleTextField,
|
|
57
|
+
GooglePassClass,
|
|
58
|
+
GooglePassObject,
|
|
59
|
+
ProfileFieldMap,
|
|
60
|
+
ProfileConfig,
|
|
61
|
+
PassGenerationResult
|
|
62
|
+
} from './types.js'
|
|
63
|
+
|
|
64
|
+
// Schema exports
|
|
65
|
+
export {
|
|
66
|
+
CreateParentInputSchema,
|
|
67
|
+
CreateChildInputSchema,
|
|
68
|
+
TimeWindowSchema,
|
|
69
|
+
CreateBusinessInputSchema,
|
|
70
|
+
CreateCustomerAccountInputSchema,
|
|
71
|
+
CreateLoyaltyProgramInputSchema,
|
|
72
|
+
IssueLoyaltyCardInputSchema,
|
|
73
|
+
UpdateLoyaltyPointsInputSchema
|
|
74
|
+
} from './types.js'
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ProfileConfig, ProfileFieldMap, HealthcareStatus } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
export const statusFlow: HealthcareStatus[] = [
|
|
4
|
+
'SCHEDULED',
|
|
5
|
+
'CHECKIN',
|
|
6
|
+
'PROCEDURE',
|
|
7
|
+
'DISCHARGED'
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export const fieldMap: ProfileFieldMap = {
|
|
11
|
+
parent: {
|
|
12
|
+
programName: { label: 'Appointment Batch', key: 'programName' },
|
|
13
|
+
site: { label: 'Location', key: 'site' },
|
|
14
|
+
windowFrom: { label: 'Date Start', key: 'window.from' },
|
|
15
|
+
windowTo: { label: 'Date End', key: 'window.to' },
|
|
16
|
+
capacity: { label: 'Total Slots', key: 'capacity' }
|
|
17
|
+
},
|
|
18
|
+
child: {
|
|
19
|
+
patientName: { label: 'Patient', key: 'patientName' },
|
|
20
|
+
doctor: { label: 'Doctor', key: 'doctor' },
|
|
21
|
+
procedure: { label: 'Procedure', key: 'procedure' },
|
|
22
|
+
status: { label: 'Status', key: 'status' },
|
|
23
|
+
parentId: { label: 'Batch ID', key: 'parentId' }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defaultTemplates = {
|
|
28
|
+
apple: {
|
|
29
|
+
parent: {
|
|
30
|
+
formatVersion: 1,
|
|
31
|
+
organizationName: 'sbcwallet Healthcare',
|
|
32
|
+
description: 'Appointment Batch',
|
|
33
|
+
backgroundColor: 'rgb(100, 149, 237)',
|
|
34
|
+
foregroundColor: 'rgb(255, 255, 255)',
|
|
35
|
+
labelColor: 'rgb(220, 230, 255)',
|
|
36
|
+
logoText: 'Health',
|
|
37
|
+
generic: {
|
|
38
|
+
primaryFields: [
|
|
39
|
+
{
|
|
40
|
+
key: 'programName',
|
|
41
|
+
label: 'Appointment Batch',
|
|
42
|
+
value: ''
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
secondaryFields: [
|
|
46
|
+
{
|
|
47
|
+
key: 'site',
|
|
48
|
+
label: 'Location',
|
|
49
|
+
value: ''
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
auxiliaryFields: [
|
|
53
|
+
{
|
|
54
|
+
key: 'windowFrom',
|
|
55
|
+
label: 'Start Date',
|
|
56
|
+
value: ''
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'windowTo',
|
|
60
|
+
label: 'End Date',
|
|
61
|
+
value: ''
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
backFields: [
|
|
65
|
+
{
|
|
66
|
+
key: 'batchId',
|
|
67
|
+
label: 'Batch ID',
|
|
68
|
+
value: ''
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'capacity',
|
|
72
|
+
label: 'Total Slots',
|
|
73
|
+
value: ''
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
child: {
|
|
79
|
+
formatVersion: 1,
|
|
80
|
+
organizationName: 'sbcwallet Healthcare',
|
|
81
|
+
description: 'Patient Visit',
|
|
82
|
+
backgroundColor: 'rgb(72, 201, 176)',
|
|
83
|
+
foregroundColor: 'rgb(255, 255, 255)',
|
|
84
|
+
labelColor: 'rgb(200, 255, 240)',
|
|
85
|
+
logoText: 'Health',
|
|
86
|
+
generic: {
|
|
87
|
+
primaryFields: [
|
|
88
|
+
{
|
|
89
|
+
key: 'patientName',
|
|
90
|
+
label: 'Patient',
|
|
91
|
+
value: ''
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
secondaryFields: [
|
|
95
|
+
{
|
|
96
|
+
key: 'doctor',
|
|
97
|
+
label: 'Doctor',
|
|
98
|
+
value: ''
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'status',
|
|
102
|
+
label: 'Status',
|
|
103
|
+
value: 'SCHEDULED'
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
auxiliaryFields: [
|
|
107
|
+
{
|
|
108
|
+
key: 'procedure',
|
|
109
|
+
label: 'Procedure',
|
|
110
|
+
value: ''
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
backFields: [
|
|
114
|
+
{
|
|
115
|
+
key: 'visitId',
|
|
116
|
+
label: 'Visit ID',
|
|
117
|
+
value: ''
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'parentId',
|
|
121
|
+
label: 'Batch ID',
|
|
122
|
+
value: ''
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
google: {
|
|
129
|
+
parentClass: {
|
|
130
|
+
issuerName: 'sbcwallet Healthcare',
|
|
131
|
+
reviewStatus: 'UNDER_REVIEW'
|
|
132
|
+
},
|
|
133
|
+
parentObject: {
|
|
134
|
+
state: 'ACTIVE',
|
|
135
|
+
cardTitle: {
|
|
136
|
+
header: 'Appointment Batch',
|
|
137
|
+
body: ''
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
childObject: {
|
|
141
|
+
state: 'ACTIVE',
|
|
142
|
+
cardTitle: {
|
|
143
|
+
header: 'Patient Visit',
|
|
144
|
+
body: ''
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const healthcareProfile: ProfileConfig = {
|
|
151
|
+
name: 'healthcare',
|
|
152
|
+
fieldMap,
|
|
153
|
+
statusFlow,
|
|
154
|
+
defaultTemplates
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default healthcareProfile
|