@thezelijah/majik-subscription 1.0.0 → 1.0.2
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/dist/enums.d.ts +38 -32
- package/dist/enums.js +32 -41
- package/dist/index.js +4 -20
- package/dist/majik-subscription.d.ts +15 -8
- package/dist/majik-subscription.js +76 -57
- package/dist/types.d.ts +15 -0
- package/dist/types.js +1 -2
- package/dist/utils.js +17 -31
- package/package.json +26 -26
package/dist/enums.d.ts
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
export declare
|
|
2
|
-
ACTIVE
|
|
3
|
-
INACTIVE
|
|
4
|
-
SUSPENDED
|
|
5
|
-
CANCELLED
|
|
6
|
-
}
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
1
|
+
export declare const SubscriptionStatus: {
|
|
2
|
+
readonly ACTIVE: "Active";
|
|
3
|
+
readonly INACTIVE: "Inactive";
|
|
4
|
+
readonly SUSPENDED: "Suspended";
|
|
5
|
+
readonly CANCELLED: "Cancelled";
|
|
6
|
+
};
|
|
7
|
+
export type SubscriptionStatus = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
|
|
8
|
+
export declare const SubscriptionType: {
|
|
9
|
+
readonly RECURRING: "Recurring";
|
|
10
|
+
readonly ONE_TIME: "One-Time";
|
|
11
|
+
readonly TRIAL: "Trial";
|
|
12
|
+
};
|
|
13
|
+
export type SubscriptionType = (typeof SubscriptionType)[keyof typeof SubscriptionType];
|
|
14
|
+
export declare const SubscriptionVisibility: {
|
|
15
|
+
readonly PUBLIC: "Public";
|
|
16
|
+
readonly PRIVATE: "Private";
|
|
17
|
+
};
|
|
18
|
+
export type SubscriptionVisibility = (typeof SubscriptionVisibility)[keyof typeof SubscriptionVisibility];
|
|
19
|
+
export declare const BillingCycle: {
|
|
20
|
+
readonly DAILY: "Daily";
|
|
21
|
+
readonly WEEKLY: "Weekly";
|
|
22
|
+
readonly MONTHLY: "Monthly";
|
|
23
|
+
readonly QUARTERLY: "Quarterly";
|
|
24
|
+
readonly YEARLY: "Yearly";
|
|
25
|
+
};
|
|
26
|
+
export type BillingCycle = (typeof BillingCycle)[keyof typeof BillingCycle];
|
|
27
|
+
export declare const RateUnit: {
|
|
28
|
+
readonly PER_SUBSCRIBER: "Per Subscriber";
|
|
29
|
+
readonly PER_ACCOUNT: "Per Account";
|
|
30
|
+
readonly PER_USER: "Per User";
|
|
31
|
+
readonly PER_MONTH: "Per Month";
|
|
32
|
+
};
|
|
33
|
+
export type RateUnit = (typeof RateUnit)[keyof typeof RateUnit];
|
|
34
|
+
export declare const CapacityPeriodResizeMode: {
|
|
35
|
+
readonly DEFAULT: "default";
|
|
36
|
+
readonly DISTRIBUTE: "distribute";
|
|
37
|
+
};
|
|
38
|
+
export type CapacityPeriodResizeMode = (typeof CapacityPeriodResizeMode)[keyof typeof CapacityPeriodResizeMode];
|
package/dist/enums.js
CHANGED
|
@@ -1,41 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
RateUnit["PER_ACCOUNT"] = "Per Account";
|
|
34
|
-
RateUnit["PER_USER"] = "Per User";
|
|
35
|
-
RateUnit["PER_MONTH"] = "Per Month";
|
|
36
|
-
})(RateUnit || (exports.RateUnit = RateUnit = {}));
|
|
37
|
-
var CapacityPeriodResizeMode;
|
|
38
|
-
(function (CapacityPeriodResizeMode) {
|
|
39
|
-
CapacityPeriodResizeMode["DEFAULT"] = "default";
|
|
40
|
-
CapacityPeriodResizeMode["DISTRIBUTE"] = "distribute";
|
|
41
|
-
})(CapacityPeriodResizeMode || (exports.CapacityPeriodResizeMode = CapacityPeriodResizeMode = {}));
|
|
1
|
+
export const SubscriptionStatus = {
|
|
2
|
+
ACTIVE: "Active",
|
|
3
|
+
INACTIVE: "Inactive",
|
|
4
|
+
SUSPENDED: "Suspended",
|
|
5
|
+
CANCELLED: "Cancelled",
|
|
6
|
+
};
|
|
7
|
+
export const SubscriptionType = {
|
|
8
|
+
RECURRING: "Recurring",
|
|
9
|
+
ONE_TIME: "One-Time",
|
|
10
|
+
TRIAL: "Trial",
|
|
11
|
+
};
|
|
12
|
+
export const SubscriptionVisibility = {
|
|
13
|
+
PUBLIC: "Public",
|
|
14
|
+
PRIVATE: "Private",
|
|
15
|
+
};
|
|
16
|
+
export const BillingCycle = {
|
|
17
|
+
DAILY: "Daily",
|
|
18
|
+
WEEKLY: "Weekly",
|
|
19
|
+
MONTHLY: "Monthly",
|
|
20
|
+
QUARTERLY: "Quarterly",
|
|
21
|
+
YEARLY: "Yearly",
|
|
22
|
+
};
|
|
23
|
+
export const RateUnit = {
|
|
24
|
+
PER_SUBSCRIBER: "Per Subscriber",
|
|
25
|
+
PER_ACCOUNT: "Per Account",
|
|
26
|
+
PER_USER: "Per User",
|
|
27
|
+
PER_MONTH: "Per Month",
|
|
28
|
+
};
|
|
29
|
+
export const CapacityPeriodResizeMode = {
|
|
30
|
+
DEFAULT: "default",
|
|
31
|
+
DISTRIBUTE: "distribute",
|
|
32
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./majik-subscription"), exports);
|
|
18
|
-
__exportStar(require("./utils"), exports);
|
|
19
|
-
__exportStar(require("./enums"), exports);
|
|
20
|
-
__exportStar(require("./types"), exports);
|
|
1
|
+
export * from "./majik-subscription";
|
|
2
|
+
export * from "./utils";
|
|
3
|
+
export * from "./enums";
|
|
4
|
+
export * from "./types";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MajikMoney } from "@thezelijah/majik-money";
|
|
2
|
-
import { COSItem, ISODateString, MonthlyCapacity, ObjectType, StartDateInput, SubscriptionID, SubscriptionMetadata, SubscriptionRate, SubscriptionSettings, YYYYMM } from "./types";
|
|
3
|
-
import { CapacityPeriodResizeMode, RateUnit, SubscriptionStatus, SubscriptionType } from "./enums";
|
|
2
|
+
import { COSItem, ISODateString, MajikSubscriptionJSON, MonthlyCapacity, ObjectType, StartDateInput, SubscriptionID, SubscriptionMetadata, SubscriptionRate, SubscriptionSettings, YYYYMM } from "./types";
|
|
3
|
+
import { BillingCycle, CapacityPeriodResizeMode, RateUnit, SubscriptionStatus, SubscriptionType } from "./enums";
|
|
4
4
|
/**
|
|
5
5
|
* Represents a subscription in the Majik system.
|
|
6
6
|
* Handles metadata, capacity, COS, and finance calculations (revenue, COS, profit, margins) for recurring subscriptions.
|
|
@@ -70,6 +70,12 @@ export declare class MajikSubscription {
|
|
|
70
70
|
* @throws Will throw an error if amount is non-positive.
|
|
71
71
|
*/
|
|
72
72
|
setRateAmount(amount: number): this;
|
|
73
|
+
/**
|
|
74
|
+
* Updates the billing cycle.
|
|
75
|
+
* @param {BillingCycle} cycle - New billing cycle (e.g., monthly, quarterly). use Enum `BillingCycle`.
|
|
76
|
+
* @returns {MajikSubscription} - Returns self for chaining.
|
|
77
|
+
*/
|
|
78
|
+
setBillingCycle(cycle: BillingCycle): this;
|
|
73
79
|
/**
|
|
74
80
|
* Updates the subscription category.
|
|
75
81
|
* @param {string} category - New category name.
|
|
@@ -337,6 +343,7 @@ export declare class MajikSubscription {
|
|
|
337
343
|
* @returns {readonly COSItem[]} - Array of COS items.
|
|
338
344
|
*/
|
|
339
345
|
get cos(): readonly COSItem[];
|
|
346
|
+
get costBreakdown(): readonly COSItem[];
|
|
340
347
|
/**
|
|
341
348
|
* Returns COS for a specific month.
|
|
342
349
|
* @param {YYYYMM} month - Month in YYYY-MM format.
|
|
@@ -403,16 +410,16 @@ export declare class MajikSubscription {
|
|
|
403
410
|
finalize(): object;
|
|
404
411
|
/**
|
|
405
412
|
* Converts the subscription instance to a plain JSON object.
|
|
406
|
-
* @returns {
|
|
413
|
+
* @returns {MajikSubscriptionJSON} - Plain object representation.
|
|
407
414
|
*/
|
|
408
|
-
toJSON():
|
|
415
|
+
toJSON(): MajikSubscriptionJSON;
|
|
409
416
|
/**
|
|
410
417
|
* Parses a plain object or JSON string into a MajikSubscription instance.
|
|
411
|
-
* @param {string |
|
|
418
|
+
* @param {string | MajikSubscriptionJSON} json - JSON string or object.
|
|
412
419
|
* @returns {MajikSubscription} - Parsed subscription instance.
|
|
413
420
|
* @throws {Error} - Throws if required properties are missing.
|
|
414
421
|
*/
|
|
415
|
-
static parseFromJSON(json: string |
|
|
422
|
+
static parseFromJSON(json: string | MajikSubscriptionJSON): MajikSubscription;
|
|
416
423
|
/**
|
|
417
424
|
* Updates the last_update timestamp to current time.
|
|
418
425
|
* Should be called whenever a property is modified.
|
|
@@ -427,5 +434,5 @@ export declare class MajikSubscription {
|
|
|
427
434
|
*/
|
|
428
435
|
private assertCurrency;
|
|
429
436
|
}
|
|
430
|
-
export declare function isMajikSubscriptionClass(item: MajikSubscription): boolean;
|
|
431
|
-
export declare function isMajikSubscriptionJSON(item: MajikSubscription): boolean;
|
|
437
|
+
export declare function isMajikSubscriptionClass(item: MajikSubscription | MajikSubscriptionJSON): boolean;
|
|
438
|
+
export declare function isMajikSubscriptionJSON(item: MajikSubscription | MajikSubscriptionJSON): boolean;
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.isMajikSubscriptionClass = isMajikSubscriptionClass;
|
|
5
|
-
exports.isMajikSubscriptionJSON = isMajikSubscriptionJSON;
|
|
6
|
-
const majik_money_1 = require("@thezelijah/majik-money");
|
|
7
|
-
const utils_1 = require("./utils");
|
|
8
|
-
const enums_1 = require("./enums");
|
|
1
|
+
import { deserializeMoney, MajikMoney, serializeMoney, } from "@thezelijah/majik-money";
|
|
2
|
+
import { autogenerateID, createEmptySubscriptionFinance, monthsInPeriod, generateSlug, isValidYYYYMM, normalizeStartDate, offsetMonthsToYYYYMM, dateToYYYYMM, } from "./utils";
|
|
3
|
+
import { CapacityPeriodResizeMode, SubscriptionStatus, SubscriptionType, SubscriptionVisibility, } from "./enums";
|
|
9
4
|
/**
|
|
10
5
|
* Represents a subscription in the Majik system.
|
|
11
6
|
* Handles metadata, capacity, COS, and finance calculations (revenue, COS, profit, margins) for recurring subscriptions.
|
|
12
7
|
*/
|
|
13
|
-
class MajikSubscription {
|
|
8
|
+
export class MajikSubscription {
|
|
9
|
+
__type = "MajikSubscription";
|
|
10
|
+
__object = "class";
|
|
11
|
+
id;
|
|
12
|
+
slug;
|
|
13
|
+
name;
|
|
14
|
+
category;
|
|
15
|
+
rate;
|
|
16
|
+
status;
|
|
17
|
+
type;
|
|
18
|
+
timestamp;
|
|
19
|
+
last_update;
|
|
20
|
+
metadata;
|
|
21
|
+
settings;
|
|
22
|
+
financeDirty = true;
|
|
14
23
|
/**
|
|
15
24
|
* Creates a new `MajikSubscription` instance.
|
|
16
25
|
* @param {SubscriptionID | undefined} id - Optional subscription ID. Auto-generated if undefined.
|
|
@@ -22,16 +31,13 @@ class MajikSubscription {
|
|
|
22
31
|
* @param {ISODateString} [last_update=new Date().toISOString()] - Optional last update timestamp.
|
|
23
32
|
*/
|
|
24
33
|
constructor(id, slug, name, metadata, settings, timestamp = new Date().toISOString(), last_update = new Date().toISOString()) {
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.financeDirty = true;
|
|
28
|
-
this.id = id || (0, utils_1.autogenerateID)("mjksub");
|
|
29
|
-
this.slug = slug || (0, utils_1.generateSlug)(name);
|
|
34
|
+
this.id = id || autogenerateID("mjksub");
|
|
35
|
+
this.slug = slug || generateSlug(name);
|
|
30
36
|
this.name = name;
|
|
31
37
|
this.metadata = metadata;
|
|
32
38
|
this.settings = settings ?? {
|
|
33
|
-
status:
|
|
34
|
-
visibility:
|
|
39
|
+
status: SubscriptionStatus.ACTIVE,
|
|
40
|
+
visibility: SubscriptionVisibility.PRIVATE,
|
|
35
41
|
system: { isRestricted: false },
|
|
36
42
|
};
|
|
37
43
|
this.type = this.metadata.type;
|
|
@@ -52,14 +58,14 @@ class MajikSubscription {
|
|
|
52
58
|
*/
|
|
53
59
|
DEFAULT_ZERO(currencyCode) {
|
|
54
60
|
const code = currencyCode || this.rate?.amount?.currency?.code || "PHP";
|
|
55
|
-
return
|
|
61
|
+
return MajikMoney.fromMinor(0, code);
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
64
|
* Initializes and creates a new `MajikSubscription` with default and null values.
|
|
59
65
|
* @param type - The type of service to initialize. Defaults to `TIME_BASED`. Use Enum `ServiceType`.
|
|
60
66
|
* @returns A new `MajikSubscription` instance.
|
|
61
67
|
*/
|
|
62
|
-
static initialize(name, type =
|
|
68
|
+
static initialize(name, type = SubscriptionType.RECURRING, rate, category = "Other", descriptionText, skuID) {
|
|
63
69
|
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
64
70
|
throw new Error("Name must be a valid non-empty string.");
|
|
65
71
|
}
|
|
@@ -76,11 +82,11 @@ class MajikSubscription {
|
|
|
76
82
|
rate: rate,
|
|
77
83
|
sku: skuID || undefined,
|
|
78
84
|
cos: [],
|
|
79
|
-
finance:
|
|
85
|
+
finance: createEmptySubscriptionFinance(rate.amount.currency.code),
|
|
80
86
|
};
|
|
81
87
|
const defaultSettings = {
|
|
82
|
-
visibility:
|
|
83
|
-
status:
|
|
88
|
+
visibility: SubscriptionVisibility.PRIVATE,
|
|
89
|
+
status: SubscriptionStatus.ACTIVE,
|
|
84
90
|
system: {
|
|
85
91
|
isRestricted: false,
|
|
86
92
|
},
|
|
@@ -95,7 +101,7 @@ class MajikSubscription {
|
|
|
95
101
|
*/
|
|
96
102
|
setName(name) {
|
|
97
103
|
this.name = name;
|
|
98
|
-
this.slug =
|
|
104
|
+
this.slug = generateSlug(name);
|
|
99
105
|
this.updateTimestamp();
|
|
100
106
|
return this;
|
|
101
107
|
}
|
|
@@ -132,8 +138,20 @@ class MajikSubscription {
|
|
|
132
138
|
setRateAmount(amount) {
|
|
133
139
|
if (amount <= 0)
|
|
134
140
|
throw new Error("Rate Amount must be positive");
|
|
135
|
-
this.rate.amount =
|
|
136
|
-
this.metadata.rate.amount =
|
|
141
|
+
this.rate.amount = MajikMoney.fromMajor(amount, this.rate.amount.currency.code);
|
|
142
|
+
this.metadata.rate.amount = MajikMoney.fromMajor(amount, this.metadata.rate.amount.currency.code);
|
|
143
|
+
this.updateTimestamp();
|
|
144
|
+
this.markFinanceDirty();
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Updates the billing cycle.
|
|
149
|
+
* @param {BillingCycle} cycle - New billing cycle (e.g., monthly, quarterly). use Enum `BillingCycle`.
|
|
150
|
+
* @returns {MajikSubscription} - Returns self for chaining.
|
|
151
|
+
*/
|
|
152
|
+
setBillingCycle(cycle) {
|
|
153
|
+
this.rate.billingCycle = cycle;
|
|
154
|
+
this.metadata.rate.billingCycle = cycle;
|
|
137
155
|
this.updateTimestamp();
|
|
138
156
|
this.markFinanceDirty();
|
|
139
157
|
return this;
|
|
@@ -213,7 +231,7 @@ class MajikSubscription {
|
|
|
213
231
|
* @throws Will throw an error if the `type` is not provided or is not a string.
|
|
214
232
|
*/
|
|
215
233
|
setType(type) {
|
|
216
|
-
if (!Object.values(
|
|
234
|
+
if (!Object.values(SubscriptionType).includes(type)) {
|
|
217
235
|
throw new Error("Invalid Subscription type.");
|
|
218
236
|
}
|
|
219
237
|
this.metadata.type = type;
|
|
@@ -252,7 +270,7 @@ class MajikSubscription {
|
|
|
252
270
|
throw new Error("COS quantity must be greater than zero");
|
|
253
271
|
this.assertCurrency(unitCost);
|
|
254
272
|
const newItem = {
|
|
255
|
-
id:
|
|
273
|
+
id: autogenerateID("mjksubcost"),
|
|
256
274
|
item: name,
|
|
257
275
|
quantity,
|
|
258
276
|
unitCost,
|
|
@@ -450,7 +468,7 @@ class MajikSubscription {
|
|
|
450
468
|
if (growthRate < 0) {
|
|
451
469
|
throw new Error("Growth rate cannot be negative");
|
|
452
470
|
}
|
|
453
|
-
const start =
|
|
471
|
+
const start = normalizeStartDate(startDate);
|
|
454
472
|
const supplyPlan = [];
|
|
455
473
|
let currentUnits = amount;
|
|
456
474
|
for (let i = 0; i < months; i++) {
|
|
@@ -496,8 +514,8 @@ class MajikSubscription {
|
|
|
496
514
|
this.markFinanceDirty();
|
|
497
515
|
return this;
|
|
498
516
|
}
|
|
499
|
-
recomputeCapacityPeriod(start, end, mode =
|
|
500
|
-
if (!
|
|
517
|
+
recomputeCapacityPeriod(start, end, mode = CapacityPeriodResizeMode.DEFAULT) {
|
|
518
|
+
if (!isValidYYYYMM(start) || !isValidYYYYMM(end)) {
|
|
501
519
|
throw new Error("Invalid YYYYMM period");
|
|
502
520
|
}
|
|
503
521
|
if (!this.hasCapacity()) {
|
|
@@ -506,21 +524,21 @@ class MajikSubscription {
|
|
|
506
524
|
if (start > end) {
|
|
507
525
|
throw new Error("Start month must be <= end month");
|
|
508
526
|
}
|
|
509
|
-
const newLength =
|
|
527
|
+
const newLength = monthsInPeriod(start, end);
|
|
510
528
|
const oldPlan = [...this.metadata.capacityPlan];
|
|
511
529
|
const oldLength = oldPlan.length;
|
|
512
530
|
const newPlan = [];
|
|
513
|
-
if (mode ===
|
|
531
|
+
if (mode === CapacityPeriodResizeMode.DEFAULT) {
|
|
514
532
|
for (let i = 0; i < newLength; i++) {
|
|
515
533
|
const source = i < oldLength ? oldPlan[i] : oldPlan[oldLength - 1]; // extend using last known value
|
|
516
534
|
newPlan.push({
|
|
517
|
-
month:
|
|
535
|
+
month: offsetMonthsToYYYYMM(start, i),
|
|
518
536
|
capacity: source.capacity,
|
|
519
537
|
adjustment: source.adjustment,
|
|
520
538
|
});
|
|
521
539
|
}
|
|
522
540
|
}
|
|
523
|
-
if (mode ===
|
|
541
|
+
if (mode === CapacityPeriodResizeMode.DISTRIBUTE) {
|
|
524
542
|
const total = this.totalCapacity;
|
|
525
543
|
const base = Math.floor(total / newLength);
|
|
526
544
|
let remainder = total % newLength;
|
|
@@ -528,7 +546,7 @@ class MajikSubscription {
|
|
|
528
546
|
const extra = remainder > 0 ? 1 : 0;
|
|
529
547
|
remainder--;
|
|
530
548
|
newPlan.push({
|
|
531
|
-
month:
|
|
549
|
+
month: offsetMonthsToYYYYMM(start, i),
|
|
532
550
|
capacity: base + extra,
|
|
533
551
|
});
|
|
534
552
|
}
|
|
@@ -546,7 +564,7 @@ class MajikSubscription {
|
|
|
546
564
|
*/
|
|
547
565
|
setCapacity(capacityPlan) {
|
|
548
566
|
capacityPlan.forEach((s) => {
|
|
549
|
-
if (!
|
|
567
|
+
if (!isValidYYYYMM(s.month))
|
|
550
568
|
throw new Error(`Invalid month: ${s.month}`);
|
|
551
569
|
if (typeof s.capacity !== "number")
|
|
552
570
|
throw new Error("Capacity must be a number");
|
|
@@ -565,10 +583,9 @@ class MajikSubscription {
|
|
|
565
583
|
* @throws Will throw an error if month already exists.
|
|
566
584
|
*/
|
|
567
585
|
addCapacity(month, units, adjustment) {
|
|
568
|
-
|
|
569
|
-
if (!(0, utils_1.isValidYYYYMM)(month))
|
|
586
|
+
if (!isValidYYYYMM(month))
|
|
570
587
|
throw new Error("Invalid month");
|
|
571
|
-
|
|
588
|
+
this.metadata.capacityPlan ??= [];
|
|
572
589
|
if (this.metadata.capacityPlan.some((s) => s.month === month)) {
|
|
573
590
|
throw new Error(`Month ${month} already exists. Use updateCapacityUnits or updateCapacityAdjustment`);
|
|
574
591
|
}
|
|
@@ -585,7 +602,7 @@ class MajikSubscription {
|
|
|
585
602
|
* @throws Will throw an error if month does not exist.
|
|
586
603
|
*/
|
|
587
604
|
updateCapacityUnits(month, units) {
|
|
588
|
-
if (!
|
|
605
|
+
if (!isValidYYYYMM(month))
|
|
589
606
|
throw new Error("Invalid month");
|
|
590
607
|
const plan = this.metadata.capacityPlan?.find((s) => s.month === month);
|
|
591
608
|
if (!plan)
|
|
@@ -603,7 +620,7 @@ class MajikSubscription {
|
|
|
603
620
|
* @throws Will throw an error if month does not exist.
|
|
604
621
|
*/
|
|
605
622
|
updateCapacityAdjustment(month, adjustment) {
|
|
606
|
-
if (!
|
|
623
|
+
if (!isValidYYYYMM(month))
|
|
607
624
|
throw new Error("Invalid month");
|
|
608
625
|
const plan = this.metadata.capacityPlan?.find((s) => s.month === month);
|
|
609
626
|
if (!plan)
|
|
@@ -620,7 +637,7 @@ class MajikSubscription {
|
|
|
620
637
|
* @throws Will throw an error if month does not exist.
|
|
621
638
|
*/
|
|
622
639
|
removeCapacity(month) {
|
|
623
|
-
if (!
|
|
640
|
+
if (!isValidYYYYMM(month))
|
|
624
641
|
throw new Error("Invalid month");
|
|
625
642
|
const index = this.metadata.capacityPlan?.findIndex((s) => s.month === month);
|
|
626
643
|
if (index === undefined || index === -1)
|
|
@@ -772,7 +789,7 @@ class MajikSubscription {
|
|
|
772
789
|
* @returns {MajikMoney} Net Revenue
|
|
773
790
|
*/
|
|
774
791
|
getNetRevenue(month, discounts, returns, allowances) {
|
|
775
|
-
if (!
|
|
792
|
+
if (!isValidYYYYMM(month))
|
|
776
793
|
throw new Error("Invalid month");
|
|
777
794
|
let net = this.getRevenue(month);
|
|
778
795
|
if (discounts)
|
|
@@ -794,7 +811,7 @@ class MajikSubscription {
|
|
|
794
811
|
* @returns {MajikMoney} Net Profit
|
|
795
812
|
*/
|
|
796
813
|
getNetProfit(month, operatingExpenses, taxes, discounts, returns, allowances) {
|
|
797
|
-
if (!
|
|
814
|
+
if (!isValidYYYYMM(month))
|
|
798
815
|
throw new Error("Invalid month");
|
|
799
816
|
let netRev = this.getNetRevenue(month, discounts, returns, allowances);
|
|
800
817
|
if (operatingExpenses)
|
|
@@ -807,7 +824,7 @@ class MajikSubscription {
|
|
|
807
824
|
* Alias for getNetProfit, same as Net Income
|
|
808
825
|
*/
|
|
809
826
|
getNetIncome(month, operatingExpenses, taxes, discounts, returns, allowances) {
|
|
810
|
-
if (!
|
|
827
|
+
if (!isValidYYYYMM(month))
|
|
811
828
|
throw new Error("Invalid month");
|
|
812
829
|
return this.getNetProfit(month, operatingExpenses, taxes, discounts, returns, allowances);
|
|
813
830
|
}
|
|
@@ -818,7 +835,7 @@ class MajikSubscription {
|
|
|
818
835
|
* @returns {MajikMoney} - Monthly revenue.
|
|
819
836
|
*/
|
|
820
837
|
getRevenue(month) {
|
|
821
|
-
if (!
|
|
838
|
+
if (!isValidYYYYMM(month))
|
|
822
839
|
throw new Error("Invalid month");
|
|
823
840
|
const plan = this.metadata.capacityPlan?.find((s) => s.month === month);
|
|
824
841
|
if (!plan)
|
|
@@ -832,13 +849,16 @@ class MajikSubscription {
|
|
|
832
849
|
get cos() {
|
|
833
850
|
return this.metadata.cos;
|
|
834
851
|
}
|
|
852
|
+
get costBreakdown() {
|
|
853
|
+
return this.cos;
|
|
854
|
+
}
|
|
835
855
|
/**
|
|
836
856
|
* Returns COS for a specific month.
|
|
837
857
|
* @param {YYYYMM} month - Month in YYYY-MM format.
|
|
838
858
|
* @returns {MajikMoney} - Monthly COS.
|
|
839
859
|
*/
|
|
840
860
|
getCOS(month) {
|
|
841
|
-
if (!
|
|
861
|
+
if (!isValidYYYYMM(month))
|
|
842
862
|
throw new Error("Invalid month");
|
|
843
863
|
const plan = this.metadata.capacityPlan?.find((s) => s.month === month);
|
|
844
864
|
if (!plan)
|
|
@@ -860,7 +880,7 @@ class MajikSubscription {
|
|
|
860
880
|
* @returns {MajikMoney} - Monthly profit.
|
|
861
881
|
*/
|
|
862
882
|
getProfit(month) {
|
|
863
|
-
if (!
|
|
883
|
+
if (!isValidYYYYMM(month))
|
|
864
884
|
throw new Error("Invalid month");
|
|
865
885
|
return this.getRevenue(month).subtract(this.getCOS(month));
|
|
866
886
|
}
|
|
@@ -870,7 +890,7 @@ class MajikSubscription {
|
|
|
870
890
|
* @returns {number} - Profit margin (0–1).
|
|
871
891
|
*/
|
|
872
892
|
getMargin(month) {
|
|
873
|
-
if (!
|
|
893
|
+
if (!isValidYYYYMM(month))
|
|
874
894
|
throw new Error("Invalid month");
|
|
875
895
|
const revenue = this.getRevenue(month);
|
|
876
896
|
return revenue.isZero()
|
|
@@ -948,7 +968,7 @@ class MajikSubscription {
|
|
|
948
968
|
/** Monthly Recurring Revenue (MRR) for a specific month or current month if not provided */
|
|
949
969
|
getMRR(month) {
|
|
950
970
|
if (!month) {
|
|
951
|
-
month =
|
|
971
|
+
month = dateToYYYYMM(new Date());
|
|
952
972
|
}
|
|
953
973
|
return this.getRevenue(month);
|
|
954
974
|
}
|
|
@@ -990,11 +1010,11 @@ class MajikSubscription {
|
|
|
990
1010
|
* @returns {object} - Serialized subscription.
|
|
991
1011
|
*/
|
|
992
1012
|
finalize() {
|
|
993
|
-
return { ...this.toJSON(), id:
|
|
1013
|
+
return { ...this.toJSON(), id: autogenerateID("mjksub") };
|
|
994
1014
|
}
|
|
995
1015
|
/**
|
|
996
1016
|
* Converts the subscription instance to a plain JSON object.
|
|
997
|
-
* @returns {
|
|
1017
|
+
* @returns {MajikSubscriptionJSON} - Plain object representation.
|
|
998
1018
|
*/
|
|
999
1019
|
toJSON() {
|
|
1000
1020
|
const preJSON = {
|
|
@@ -1012,17 +1032,17 @@ class MajikSubscription {
|
|
|
1012
1032
|
metadata: this.metadata,
|
|
1013
1033
|
settings: this.settings,
|
|
1014
1034
|
};
|
|
1015
|
-
return
|
|
1035
|
+
return serializeMoney(preJSON);
|
|
1016
1036
|
}
|
|
1017
1037
|
/**
|
|
1018
1038
|
* Parses a plain object or JSON string into a MajikSubscription instance.
|
|
1019
|
-
* @param {string |
|
|
1039
|
+
* @param {string | MajikSubscriptionJSON} json - JSON string or object.
|
|
1020
1040
|
* @returns {MajikSubscription} - Parsed subscription instance.
|
|
1021
1041
|
* @throws {Error} - Throws if required properties are missing.
|
|
1022
1042
|
*/
|
|
1023
1043
|
static parseFromJSON(json) {
|
|
1024
1044
|
const rawParse = typeof json === "string" ? JSON.parse(json) : structuredClone(json);
|
|
1025
|
-
const parsedData =
|
|
1045
|
+
const parsedData = deserializeMoney(rawParse);
|
|
1026
1046
|
if (!parsedData.id)
|
|
1027
1047
|
throw new Error("Missing required property: 'id'");
|
|
1028
1048
|
if (!parsedData.timestamp)
|
|
@@ -1053,10 +1073,9 @@ class MajikSubscription {
|
|
|
1053
1073
|
}
|
|
1054
1074
|
}
|
|
1055
1075
|
}
|
|
1056
|
-
|
|
1057
|
-
function isMajikSubscriptionClass(item) {
|
|
1076
|
+
export function isMajikSubscriptionClass(item) {
|
|
1058
1077
|
return item.__object === "class";
|
|
1059
1078
|
}
|
|
1060
|
-
function isMajikSubscriptionJSON(item) {
|
|
1079
|
+
export function isMajikSubscriptionJSON(item) {
|
|
1061
1080
|
return item.__object === "json";
|
|
1062
1081
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -93,3 +93,18 @@ export interface SubscriptionSettings {
|
|
|
93
93
|
restrictedUntil?: ISODateString;
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
export interface MajikSubscriptionJSON {
|
|
97
|
+
__type: "MajikSubscription";
|
|
98
|
+
__object: "json";
|
|
99
|
+
id: SubscriptionID;
|
|
100
|
+
slug: string;
|
|
101
|
+
name: string;
|
|
102
|
+
category: string;
|
|
103
|
+
rate: SubscriptionRate;
|
|
104
|
+
status: SubscriptionStatus;
|
|
105
|
+
type: SubscriptionType;
|
|
106
|
+
timestamp: ISODateString;
|
|
107
|
+
last_update: ISODateString;
|
|
108
|
+
metadata: SubscriptionMetadata;
|
|
109
|
+
settings: SubscriptionSettings;
|
|
110
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.generateSlug = generateSlug;
|
|
4
|
-
exports.autogenerateID = autogenerateID;
|
|
5
|
-
exports.isValidYYYYMM = isValidYYYYMM;
|
|
6
|
-
exports.createZeroValueRatio = createZeroValueRatio;
|
|
7
|
-
exports.createEmptySubscriptionFinance = createEmptySubscriptionFinance;
|
|
8
|
-
exports.isoToYYYYMM = isoToYYYYMM;
|
|
9
|
-
exports.yyyyMMToISO = yyyyMMToISO;
|
|
10
|
-
exports.dateToYYYYMM = dateToYYYYMM;
|
|
11
|
-
exports.yyyyMMToDate = yyyyMMToDate;
|
|
12
|
-
exports.offsetMonthsToYYYYMM = offsetMonthsToYYYYMM;
|
|
13
|
-
exports.monthsInPeriod = monthsInPeriod;
|
|
14
|
-
exports.normalizeStartDate = normalizeStartDate;
|
|
15
|
-
const nanoid_1 = require("nanoid");
|
|
16
|
-
const majik_money_1 = require("@thezelijah/majik-money");
|
|
1
|
+
import { customAlphabet } from "nanoid";
|
|
2
|
+
import { MajikMoney } from "@thezelijah/majik-money";
|
|
17
3
|
/**
|
|
18
4
|
* Generates a URL-friendly slug from the name,
|
|
19
5
|
* appending a Unix timestamp to ensure uniqueness.
|
|
@@ -21,9 +7,9 @@ const majik_money_1 = require("@thezelijah/majik-money");
|
|
|
21
7
|
* @param name - The name to base the slug on.
|
|
22
8
|
* @returns A unique slug string.
|
|
23
9
|
*/
|
|
24
|
-
function generateSlug(name) {
|
|
10
|
+
export function generateSlug(name) {
|
|
25
11
|
// Create the generator function ONCE with your custom alphabet and length
|
|
26
|
-
const generateSlugID =
|
|
12
|
+
const generateSlugID = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6);
|
|
27
13
|
const genUID = generateSlugID(); // e.g., 'X4tF9z'
|
|
28
14
|
const slugText = name
|
|
29
15
|
.toLowerCase()
|
|
@@ -37,24 +23,24 @@ function generateSlug(name) {
|
|
|
37
23
|
* @param prefix - The prefix string name to add.
|
|
38
24
|
* @returns A unique ID string prefixed.
|
|
39
25
|
*/
|
|
40
|
-
function autogenerateID(prefix = "majik") {
|
|
26
|
+
export function autogenerateID(prefix = "majik") {
|
|
41
27
|
// Create the generator function ONCE with your custom alphabet and length
|
|
42
|
-
const generateID =
|
|
28
|
+
const generateID = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 8);
|
|
43
29
|
// Call the generator function to produce the actual ID string
|
|
44
30
|
const genUID = generateID(); // Example output: 'G7K2aZp9'
|
|
45
31
|
return `${prefix}-${genUID}`;
|
|
46
32
|
}
|
|
47
|
-
function isValidYYYYMM(month) {
|
|
33
|
+
export function isValidYYYYMM(month) {
|
|
48
34
|
return /^\d{4}-0[1-9]|1[0-2]$/.test(month);
|
|
49
35
|
}
|
|
50
|
-
function createZeroValueRatio(currencyCode) {
|
|
51
|
-
const zero =
|
|
36
|
+
export function createZeroValueRatio(currencyCode) {
|
|
37
|
+
const zero = MajikMoney.fromMinor(0, currencyCode);
|
|
52
38
|
return {
|
|
53
39
|
value: zero,
|
|
54
40
|
marginRatio: 0,
|
|
55
41
|
};
|
|
56
42
|
}
|
|
57
|
-
function createEmptySubscriptionFinance(currencyCode) {
|
|
43
|
+
export function createEmptySubscriptionFinance(currencyCode) {
|
|
58
44
|
return {
|
|
59
45
|
revenue: {
|
|
60
46
|
gross: createZeroValueRatio(currencyCode),
|
|
@@ -74,7 +60,7 @@ function createEmptySubscriptionFinance(currencyCode) {
|
|
|
74
60
|
},
|
|
75
61
|
};
|
|
76
62
|
}
|
|
77
|
-
function isoToYYYYMM(isoDate) {
|
|
63
|
+
export function isoToYYYYMM(isoDate) {
|
|
78
64
|
const date = new Date(isoDate);
|
|
79
65
|
if (isNaN(date.getTime())) {
|
|
80
66
|
throw new Error("Invalid ISO date");
|
|
@@ -84,12 +70,12 @@ function isoToYYYYMM(isoDate) {
|
|
|
84
70
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
85
71
|
return `${year}-${month}`;
|
|
86
72
|
}
|
|
87
|
-
function yyyyMMToISO(yyyyMM) {
|
|
73
|
+
export function yyyyMMToISO(yyyyMM) {
|
|
88
74
|
const [year, month] = yyyyMM.split("-").map(Number);
|
|
89
75
|
// Construct as UTC Midnight
|
|
90
76
|
return new Date(Date.UTC(year, month - 1, 1)).toISOString();
|
|
91
77
|
}
|
|
92
|
-
function dateToYYYYMM(date) {
|
|
78
|
+
export function dateToYYYYMM(date) {
|
|
93
79
|
if (isNaN(date.getTime())) {
|
|
94
80
|
throw new Error("Invalid Date object");
|
|
95
81
|
}
|
|
@@ -97,11 +83,11 @@ function dateToYYYYMM(date) {
|
|
|
97
83
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
98
84
|
return `${year}-${month}`;
|
|
99
85
|
}
|
|
100
|
-
function yyyyMMToDate(yyyyMM) {
|
|
86
|
+
export function yyyyMMToDate(yyyyMM) {
|
|
101
87
|
const [year, month] = yyyyMM.split("-").map(Number);
|
|
102
88
|
return new Date(Date.UTC(year, month - 1, 1));
|
|
103
89
|
}
|
|
104
|
-
function offsetMonthsToYYYYMM(input, offsetMonths) {
|
|
90
|
+
export function offsetMonthsToYYYYMM(input, offsetMonths) {
|
|
105
91
|
const date = normalizeStartDate(input);
|
|
106
92
|
// Since normalizeStartDate now returns a UTC date,
|
|
107
93
|
// we can safely use UTC methods here.
|
|
@@ -110,7 +96,7 @@ function offsetMonthsToYYYYMM(input, offsetMonths) {
|
|
|
110
96
|
const result = new Date(Date.UTC(year, month, 1));
|
|
111
97
|
return dateToYYYYMM(result);
|
|
112
98
|
}
|
|
113
|
-
function monthsInPeriod(earlier, later) {
|
|
99
|
+
export function monthsInPeriod(earlier, later) {
|
|
114
100
|
const start = yyyyMMToDate(earlier);
|
|
115
101
|
const end = yyyyMMToDate(later);
|
|
116
102
|
const startYear = start.getUTCFullYear();
|
|
@@ -119,7 +105,7 @@ function monthsInPeriod(earlier, later) {
|
|
|
119
105
|
const endMonth = end.getUTCMonth();
|
|
120
106
|
return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
|
|
121
107
|
}
|
|
122
|
-
function normalizeStartDate(input) {
|
|
108
|
+
export function normalizeStartDate(input) {
|
|
123
109
|
if (!input) {
|
|
124
110
|
const now = new Date();
|
|
125
111
|
// Return UTC version of the first of the current month
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thezelijah/majik-subscription",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Majik Subscription is a fully-featured class representing a subscription-based offering in the Majik system, designed for recurring revenue modeling, cost tracking, and subscriber capacity planning. It provides utilities for computing MRR, ARR, revenue, profit, margins, Cost of Subscription (COS), and net income on a per-period basis. Chainable setter methods make it easy to construct and update subscriptions fluently.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Zelijah",
|
|
@@ -15,30 +15,30 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/jedlsf/majik-subscription.git"
|
|
17
17
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"majik",
|
|
20
|
+
"majik-subscription",
|
|
21
|
+
"subscription-management",
|
|
22
|
+
"subscription-finance",
|
|
23
|
+
"cost-of-subscription",
|
|
24
|
+
"COS",
|
|
25
|
+
"revenue-calculation",
|
|
26
|
+
"profit-calculation",
|
|
27
|
+
"financial-modeling",
|
|
28
|
+
"capacity-planning",
|
|
29
|
+
"resource-planning",
|
|
30
|
+
"timesheet",
|
|
31
|
+
"billing",
|
|
32
|
+
"pricing",
|
|
33
|
+
"business-tools",
|
|
34
|
+
"typescript",
|
|
35
|
+
"javascript",
|
|
36
|
+
"enterprise-tools",
|
|
37
|
+
"monetary-calculation",
|
|
38
|
+
"majik-money",
|
|
39
|
+
"subscription-analytics",
|
|
40
|
+
"forecasting"
|
|
41
|
+
],
|
|
42
42
|
"homepage": "https://github.com/jedlsf/majik-subscription#readme",
|
|
43
43
|
"bugs": {
|
|
44
44
|
"url": "https://github.com/jedlsf/majik-subscription/issues"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"prepublishOnly": "npm run build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@thezelijah/majik-money": "^1.0.
|
|
52
|
+
"@thezelijah/majik-money": "^1.0.4",
|
|
53
53
|
"nanoid": "^5.1.6",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
}
|