@voyantjs/bookings 0.8.0 → 0.10.0

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 (47) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/markets-ref.d.ts +151 -0
  5. package/dist/markets-ref.d.ts.map +1 -0
  6. package/dist/markets-ref.js +19 -0
  7. package/dist/pii-redaction.d.ts +89 -0
  8. package/dist/pii-redaction.d.ts.map +1 -0
  9. package/dist/pii-redaction.js +120 -0
  10. package/dist/pii.d.ts +1 -0
  11. package/dist/pii.d.ts.map +1 -1
  12. package/dist/pii.js +20 -1
  13. package/dist/routes-groups.d.ts +3 -2
  14. package/dist/routes-groups.d.ts.map +1 -1
  15. package/dist/routes-public.d.ts +11 -13
  16. package/dist/routes-public.d.ts.map +1 -1
  17. package/dist/routes-public.js +3 -3
  18. package/dist/routes.d.ts +16 -8
  19. package/dist/routes.d.ts.map +1 -1
  20. package/dist/routes.js +57 -9
  21. package/dist/schema/travel-details.d.ts +37 -0
  22. package/dist/schema/travel-details.d.ts.map +1 -1
  23. package/dist/schema/travel-details.js +6 -0
  24. package/dist/schema-core.d.ts +17 -17
  25. package/dist/schema-core.d.ts.map +1 -1
  26. package/dist/schema-core.js +8 -2
  27. package/dist/schema-items.d.ts.map +1 -1
  28. package/dist/schema-items.js +6 -1
  29. package/dist/service-public.d.ts +0 -6
  30. package/dist/service-public.d.ts.map +1 -1
  31. package/dist/service-public.js +0 -4
  32. package/dist/service.d.ts +55 -46
  33. package/dist/service.d.ts.map +1 -1
  34. package/dist/service.js +288 -89
  35. package/dist/state-machine.d.ts +29 -0
  36. package/dist/state-machine.d.ts.map +1 -0
  37. package/dist/state-machine.js +39 -0
  38. package/dist/validation-public.d.ts +0 -6
  39. package/dist/validation-public.d.ts.map +1 -1
  40. package/dist/validation-public.js +0 -2
  41. package/dist/validation.d.ts +0 -4
  42. package/dist/validation.d.ts.map +1 -1
  43. package/dist/validation.js +0 -2
  44. package/dist/workflows/refund-booking.d.ts +87 -0
  45. package/dist/workflows/refund-booking.d.ts.map +1 -0
  46. package/dist/workflows/refund-booking.js +210 -0
  47. package/package.json +7 -6
@@ -366,7 +366,6 @@ export declare const insertTravelerSchema: z.ZodObject<{
366
366
  email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
367
367
  phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
368
368
  preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
369
- accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
370
369
  specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
371
370
  travelerCategory: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
372
371
  other: "other";
@@ -384,7 +383,6 @@ export declare const updateTravelerSchema: z.ZodObject<{
384
383
  email: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
385
384
  phone: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
386
385
  preferredLanguage: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
387
- accessibilityNeeds: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
388
386
  specialRequests: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
389
387
  travelerCategory: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodEnum<{
390
388
  other: "other";
@@ -415,7 +413,6 @@ export declare const insertTravelerRecordSchema: z.ZodObject<{
415
413
  email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
416
414
  phone: z.ZodNullable<z.ZodOptional<z.ZodString>>;
417
415
  preferredLanguage: z.ZodNullable<z.ZodOptional<z.ZodString>>;
418
- accessibilityNeeds: z.ZodNullable<z.ZodOptional<z.ZodString>>;
419
416
  specialRequests: z.ZodNullable<z.ZodOptional<z.ZodString>>;
420
417
  isPrimary: z.ZodDefault<z.ZodBoolean>;
421
418
  notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
@@ -439,7 +436,6 @@ export declare const updateTravelerRecordSchema: z.ZodObject<{
439
436
  email: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
440
437
  phone: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
441
438
  preferredLanguage: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
442
- accessibilityNeeds: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
443
439
  specialRequests: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
444
440
  isPrimary: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
445
441
  notes: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -194,7 +194,6 @@ const travelerRecordCoreSchema = z.object({
194
194
  email: z.string().email().optional().nullable(),
195
195
  phone: z.string().max(50).optional().nullable(),
196
196
  preferredLanguage: z.string().max(35).optional().nullable(),
197
- accessibilityNeeds: z.string().optional().nullable(),
198
197
  specialRequests: z.string().optional().nullable(),
199
198
  isPrimary: z.boolean().default(false),
200
199
  notes: z.string().optional().nullable(),
@@ -206,7 +205,6 @@ const travelerCoreSchema = z.object({
206
205
  email: z.string().email().optional().nullable(),
207
206
  phone: z.string().max(50).optional().nullable(),
208
207
  preferredLanguage: z.string().max(35).optional().nullable(),
209
- accessibilityNeeds: z.string().optional().nullable(),
210
208
  specialRequests: z.string().optional().nullable(),
211
209
  travelerCategory: bookingTravelerCategorySchema.optional().nullable(),
212
210
  isPrimary: z.boolean().optional().nullable(),
@@ -0,0 +1,87 @@
1
+ import { type EventBus } from "@voyantjs/core";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ /**
4
+ * Input passed when starting a refund.
5
+ */
6
+ export interface RefundBookingInput {
7
+ bookingId: string;
8
+ /** Free-form audit reason. Required for ops + customer comms. */
9
+ reason: string;
10
+ /**
11
+ * Refund amount in cents. Pass `null` to refund the booking's full
12
+ * `sellAmountCents`; pass a smaller value for a partial refund.
13
+ */
14
+ amountCents?: number | null;
15
+ /** User triggering the refund (for audit). */
16
+ userId?: string;
17
+ }
18
+ /**
19
+ * Side-effect dependencies — supplied by the caller. Decouples the saga
20
+ * from finance + transactions + notifications packages so this can ship
21
+ * without those modules being imported.
22
+ *
23
+ * - `createCreditNote` is expected to be transactional internally and to
24
+ * return a credit note id. Pass a no-op when there's no payment to refund.
25
+ * - `voidCreditNote` is the compensation; it should mark the credit note
26
+ * void (or delete it) so a retry of the saga doesn't double-credit.
27
+ * - `reverseSupplierOffer` updates linked supplier offer/order rows. May
28
+ * be a no-op if no transaction link exists.
29
+ * - `notifyCustomer` is fire-and-forget; failures don't trigger
30
+ * compensation (notifications fail closed elsewhere).
31
+ */
32
+ export interface RefundBookingDeps {
33
+ db: PostgresJsDatabase;
34
+ eventBus?: EventBus;
35
+ createCreditNote: (args: {
36
+ bookingId: string;
37
+ amountCents: number;
38
+ reason: string;
39
+ }) => Promise<{
40
+ creditNoteId: string;
41
+ } | null>;
42
+ voidCreditNote?: (args: {
43
+ creditNoteId: string;
44
+ reason: string;
45
+ }) => Promise<void>;
46
+ reverseSupplierOffer?: (args: {
47
+ bookingId: string;
48
+ reason: string;
49
+ }) => Promise<void>;
50
+ notifyCustomer?: (args: {
51
+ bookingId: string;
52
+ reason: string;
53
+ amountCents: number;
54
+ }) => Promise<void>;
55
+ }
56
+ /**
57
+ * Build the refund saga for a booking.
58
+ *
59
+ * **Steps**
60
+ *
61
+ * 1. `validate-state` — load the booking, ensure it's in a refundable
62
+ * status (`confirmed`, `in_progress`, or `on_hold`), compute the refund
63
+ * amount. Compensation: none — this step doesn't mutate.
64
+ * 2. `create-credit-note` — call into finance via the injected dep.
65
+ * Compensation: void the credit note.
66
+ * 3. `release-inventory` — release any held allocations + the slot
67
+ * capacity (only when the booking hasn't started yet). Compensation:
68
+ * re-acquire (best-effort; if inventory has since been re-sold,
69
+ * that's a known limitation logged for ops triage).
70
+ * 4. `reverse-supplier-offer` — best-effort call into transactions.
71
+ * Compensation: none (idempotent re-call expected if needed).
72
+ * 5. `transition-booking` — flip the booking to `cancelled`. Last
73
+ * DB-touching step so a failure here doesn't leave the booking
74
+ * cancelled with no credit-note rollback.
75
+ * 6. `emit` — fire `booking.refunded` after-commit. Best-effort; no
76
+ * compensation.
77
+ * 7. `notify` — async-style notification. Best-effort; no compensation.
78
+ *
79
+ * The saga always runs in a single host process — durability for async
80
+ * notifications is the deployment's responsibility (see `JobRunner`).
81
+ */
82
+ export declare function buildRefundBookingWorkflow(deps: RefundBookingDeps): import("@voyantjs/core").WorkflowDefinition;
83
+ /**
84
+ * Convenience wrapper: build + run the saga in one call.
85
+ */
86
+ export declare function refundBooking(input: RefundBookingInput, deps: RefundBookingDeps): Promise<import("@voyantjs/core").WorkflowResult>;
87
+ //# sourceMappingURL=refund-booking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refund-booking.d.ts","sourceRoot":"","sources":["../../src/workflows/refund-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,QAAQ,EAAQ,MAAM,gBAAgB,CAAA;AAEpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAMjE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,gBAAgB,EAAE,CAAC,IAAI,EAAE;QACvB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,KAAK,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAC9C,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrF,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QACtB,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,WAAW,EAAE,MAAM,CAAA;KACpB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iBAAiB,+CAgMjE;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,iBAAiB,oDAGrF"}
@@ -0,0 +1,210 @@
1
+ import { createWorkflow, step } from "@voyantjs/core";
2
+ import { eq, sql } from "drizzle-orm";
3
+ import { availabilitySlotsRef } from "../availability-ref.js";
4
+ import { bookingActivityLog, bookingAllocations, bookingItems, bookings } from "../schema.js";
5
+ import { transitionBooking } from "../state-machine.js";
6
+ /**
7
+ * Build the refund saga for a booking.
8
+ *
9
+ * **Steps**
10
+ *
11
+ * 1. `validate-state` — load the booking, ensure it's in a refundable
12
+ * status (`confirmed`, `in_progress`, or `on_hold`), compute the refund
13
+ * amount. Compensation: none — this step doesn't mutate.
14
+ * 2. `create-credit-note` — call into finance via the injected dep.
15
+ * Compensation: void the credit note.
16
+ * 3. `release-inventory` — release any held allocations + the slot
17
+ * capacity (only when the booking hasn't started yet). Compensation:
18
+ * re-acquire (best-effort; if inventory has since been re-sold,
19
+ * that's a known limitation logged for ops triage).
20
+ * 4. `reverse-supplier-offer` — best-effort call into transactions.
21
+ * Compensation: none (idempotent re-call expected if needed).
22
+ * 5. `transition-booking` — flip the booking to `cancelled`. Last
23
+ * DB-touching step so a failure here doesn't leave the booking
24
+ * cancelled with no credit-note rollback.
25
+ * 6. `emit` — fire `booking.refunded` after-commit. Best-effort; no
26
+ * compensation.
27
+ * 7. `notify` — async-style notification. Best-effort; no compensation.
28
+ *
29
+ * The saga always runs in a single host process — durability for async
30
+ * notifications is the deployment's responsibility (see `JobRunner`).
31
+ */
32
+ export function buildRefundBookingWorkflow(deps) {
33
+ return createWorkflow("refund-booking", [
34
+ step("validate-state").run(async (input) => {
35
+ const [row] = await deps.db
36
+ .select({
37
+ id: bookings.id,
38
+ bookingNumber: bookings.bookingNumber,
39
+ status: bookings.status,
40
+ sellAmountCents: bookings.sellAmountCents,
41
+ })
42
+ .from(bookings)
43
+ .where(eq(bookings.id, input.bookingId))
44
+ .limit(1);
45
+ if (!row) {
46
+ throw new Error(`refund-booking: booking ${input.bookingId} not found`);
47
+ }
48
+ if (row.status !== "confirmed" && row.status !== "in_progress" && row.status !== "on_hold") {
49
+ throw new Error(`refund-booking: booking ${input.bookingId} is in ${row.status}, not refundable`);
50
+ }
51
+ const fullRefundAmount = row.sellAmountCents ?? 0;
52
+ const requested = input.amountCents ?? fullRefundAmount;
53
+ if (requested < 0 || requested > fullRefundAmount) {
54
+ throw new Error(`refund-booking: requested amount ${requested} out of range [0, ${fullRefundAmount}]`);
55
+ }
56
+ return {
57
+ bookingId: row.id,
58
+ bookingNumber: row.bookingNumber,
59
+ previousStatus: row.status,
60
+ fullRefundAmount,
61
+ refundAmount: requested,
62
+ };
63
+ }),
64
+ step("create-credit-note")
65
+ .run(async (input, ctx) => {
66
+ const validate = ctx.results["validate-state"];
67
+ if (validate.refundAmount === 0) {
68
+ // Draft / unpaid booking — no payment to refund. Short-circuit.
69
+ return { creditNoteId: null, amountCents: 0 };
70
+ }
71
+ const created = await deps.createCreditNote({
72
+ bookingId: input.bookingId,
73
+ amountCents: validate.refundAmount,
74
+ reason: input.reason,
75
+ });
76
+ return {
77
+ creditNoteId: created?.creditNoteId ?? null,
78
+ amountCents: validate.refundAmount,
79
+ };
80
+ })
81
+ .compensate(async (output) => {
82
+ if (output.creditNoteId && deps.voidCreditNote) {
83
+ await deps.voidCreditNote({
84
+ creditNoteId: output.creditNoteId,
85
+ reason: "refund-saga rolled back",
86
+ });
87
+ }
88
+ }),
89
+ step("release-inventory")
90
+ .run(async (input) => {
91
+ return await deps.db.transaction(async (tx) => {
92
+ const allocs = await tx
93
+ .select()
94
+ .from(bookingAllocations)
95
+ .where(eq(bookingAllocations.bookingId, input.bookingId));
96
+ const releaseable = allocs.filter((a) => a.status === "held" || a.status === "confirmed");
97
+ for (const allocation of releaseable) {
98
+ if (allocation.availabilitySlotId) {
99
+ // Best-effort capacity release — restore the held quantity.
100
+ await tx
101
+ .update(availabilitySlotsRef)
102
+ .set({
103
+ remainingPax: sql `${availabilitySlotsRef.remainingPax} + ${allocation.quantity}`,
104
+ })
105
+ .where(eq(availabilitySlotsRef.id, allocation.availabilitySlotId));
106
+ }
107
+ }
108
+ if (releaseable.length > 0) {
109
+ const releaseableIds = releaseable.map((a) => a.id);
110
+ await tx
111
+ .update(bookingAllocations)
112
+ .set({ status: "released", releasedAt: new Date(), updatedAt: new Date() })
113
+ .where(sql `${bookingAllocations.id} IN (${sql.join(releaseableIds.map((id) => sql `${id}`), sql `, `)})`);
114
+ }
115
+ await tx
116
+ .update(bookingItems)
117
+ .set({ status: "cancelled", updatedAt: new Date() })
118
+ .where(eq(bookingItems.bookingId, input.bookingId));
119
+ return {
120
+ releasedAllocationIds: releaseable.map((a) => a.id),
121
+ slotIds: releaseable
122
+ .map((a) => a.availabilitySlotId)
123
+ .filter((id) => Boolean(id)),
124
+ };
125
+ });
126
+ })
127
+ .compensate(async (output) => {
128
+ // Re-decrement the slots we restored. Note: if the slot has since
129
+ // been re-sold this will fail loudly — that's intentional, an
130
+ // operator must intervene.
131
+ if (output.slotIds.length === 0)
132
+ return;
133
+ await deps.db.transaction(async (tx) => {
134
+ for (const slotId of output.slotIds) {
135
+ await tx
136
+ .update(availabilitySlotsRef)
137
+ .set({ remainingPax: sql `${availabilitySlotsRef.remainingPax} - 1` })
138
+ .where(eq(availabilitySlotsRef.id, slotId));
139
+ }
140
+ });
141
+ }),
142
+ step("reverse-supplier-offer").run(async (input) => {
143
+ if (!deps.reverseSupplierOffer)
144
+ return { reversed: false };
145
+ await deps.reverseSupplierOffer({ bookingId: input.bookingId, reason: input.reason });
146
+ return { reversed: true };
147
+ }),
148
+ step("transition-booking").run(async (input, ctx) => {
149
+ const validate = ctx.results["validate-state"];
150
+ const patch = transitionBooking(validate.previousStatus, "cancelled");
151
+ await deps.db.transaction(async (tx) => {
152
+ await tx
153
+ .update(bookings)
154
+ .set({ ...patch, updatedAt: new Date() })
155
+ .where(eq(bookings.id, input.bookingId));
156
+ await tx.insert(bookingActivityLog).values({
157
+ bookingId: input.bookingId,
158
+ actorId: input.userId ?? "system",
159
+ activityType: "status_change",
160
+ description: `Refunded from ${validate.previousStatus}: ${input.reason}`,
161
+ metadata: {
162
+ oldStatus: validate.previousStatus,
163
+ newStatus: "cancelled",
164
+ refundAmountCents: validate.refundAmount,
165
+ reason: input.reason,
166
+ },
167
+ });
168
+ });
169
+ return { status: "cancelled" };
170
+ }),
171
+ step("emit").run(async (input, ctx) => {
172
+ const validate = ctx.results["validate-state"];
173
+ if (!deps.eventBus)
174
+ return { emitted: false };
175
+ await deps.eventBus.emit("booking.refunded", {
176
+ bookingId: input.bookingId,
177
+ bookingNumber: validate.bookingNumber,
178
+ previousStatus: validate.previousStatus,
179
+ refundAmountCents: validate.refundAmount,
180
+ reason: input.reason,
181
+ actorId: input.userId ?? null,
182
+ }, { category: "domain", source: "service" });
183
+ return { emitted: true };
184
+ }),
185
+ step("notify").run(async (input, ctx) => {
186
+ if (!deps.notifyCustomer)
187
+ return { notified: false };
188
+ const validate = ctx.results["validate-state"];
189
+ try {
190
+ await deps.notifyCustomer({
191
+ bookingId: input.bookingId,
192
+ reason: input.reason,
193
+ amountCents: validate.refundAmount,
194
+ });
195
+ }
196
+ catch {
197
+ // Notifications are best-effort. Log via the deployment's logger.
198
+ return { notified: false };
199
+ }
200
+ return { notified: true };
201
+ }),
202
+ ]);
203
+ }
204
+ /**
205
+ * Convenience wrapper: build + run the saga in one call.
206
+ */
207
+ export async function refundBooking(input, deps) {
208
+ const wf = buildRefundBookingWorkflow(deps);
209
+ return wf.run({ input });
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/bookings",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "license": "FSL-1.1-Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -49,14 +49,15 @@
49
49
  "drizzle-orm": "^0.45.2",
50
50
  "hono": "^4.12.10",
51
51
  "zod": "^4.3.6",
52
- "@voyantjs/core": "0.8.0",
53
- "@voyantjs/db": "0.8.0",
54
- "@voyantjs/hono": "0.8.0",
55
- "@voyantjs/utils": "0.8.0"
52
+ "@voyantjs/core": "0.10.0",
53
+ "@voyantjs/db": "0.10.0",
54
+ "@voyantjs/hono": "0.10.0",
55
+ "@voyantjs/utils": "0.10.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "typescript": "^6.0.2",
59
- "@voyantjs/products": "0.8.0",
59
+ "@voyantjs/markets": "0.10.0",
60
+ "@voyantjs/products": "0.10.0",
60
61
  "@voyantjs/voyant-typescript-config": "0.1.0"
61
62
  },
62
63
  "files": [