@thezelijah/majik-subscription 1.0.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.
@@ -0,0 +1,95 @@
1
+ import { MajikMoney } from "@thezelijah/majik-money";
2
+ import { BillingCycle, RateUnit, SubscriptionStatus, SubscriptionType, SubscriptionVisibility } from "./enums";
3
+ export type ObjectType = "class" | "json";
4
+ export type SubscriptionID = string;
5
+ export type SubscriptionSKU = string;
6
+ export type ISODateString = string;
7
+ export type YYYYMM = `${number}${number}${number}${number}-${number}${number}`;
8
+ export type StartDateInput = Date | ISODateString | YYYYMM;
9
+ /**
10
+ * Represents a Cost of Subscription (COS) item.
11
+ * E.g., infrastructure, hosting, support, software licenses.
12
+ */
13
+ export interface COSItem {
14
+ id: string;
15
+ item: string;
16
+ unitCost: MajikMoney;
17
+ quantity: number;
18
+ subtotal: MajikMoney;
19
+ unit?: string;
20
+ }
21
+ /**
22
+ * Optional monthly subscription capacity plan entry.
23
+ * Could represent max allowed subscribers or seats per month.
24
+ */
25
+ export interface MonthlyCapacity {
26
+ month: YYYYMM;
27
+ capacity: number;
28
+ adjustment?: number;
29
+ }
30
+ /**
31
+ * Value with margin ratio for finance snapshots.
32
+ */
33
+ export interface ValueRatio {
34
+ value: MajikMoney;
35
+ marginRatio: number;
36
+ }
37
+ /**
38
+ * Subscription finance information.
39
+ */
40
+ export interface SubscriptionFinance {
41
+ profit: {
42
+ gross: ValueRatio;
43
+ net: ValueRatio;
44
+ };
45
+ revenue: {
46
+ gross: ValueRatio;
47
+ net: ValueRatio;
48
+ };
49
+ income: {
50
+ gross: ValueRatio;
51
+ net: ValueRatio;
52
+ };
53
+ cos: {
54
+ gross: ValueRatio;
55
+ net: ValueRatio;
56
+ };
57
+ }
58
+ /**
59
+ * Subscription rate object: amount + billing unit.
60
+ */
61
+ export interface SubscriptionRate {
62
+ amount: MajikMoney;
63
+ unit: RateUnit;
64
+ billingCycle: BillingCycle;
65
+ }
66
+ /**
67
+ * Metadata of a subscription.
68
+ */
69
+ export interface SubscriptionMetadata {
70
+ sku?: SubscriptionSKU;
71
+ description: {
72
+ text: string;
73
+ html?: string;
74
+ seo?: string;
75
+ };
76
+ photos?: string[];
77
+ type: SubscriptionType;
78
+ category: string;
79
+ rate: SubscriptionRate;
80
+ cos: COSItem[];
81
+ capacityPlan?: MonthlyCapacity[];
82
+ /** Cached finance snapshot */
83
+ finance: SubscriptionFinance;
84
+ }
85
+ /**
86
+ * Subscription settings including visibility and status.
87
+ */
88
+ export interface SubscriptionSettings {
89
+ status: SubscriptionStatus;
90
+ visibility: SubscriptionVisibility;
91
+ system?: {
92
+ isRestricted: boolean;
93
+ restrictedUntil?: ISODateString;
94
+ };
95
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ import { SubscriptionFinance, StartDateInput, ValueRatio, YYYYMM } from "./types";
2
+ /**
3
+ * Generates a URL-friendly slug from the name,
4
+ * appending a Unix timestamp to ensure uniqueness.
5
+ *
6
+ * @param name - The name to base the slug on.
7
+ * @returns A unique slug string.
8
+ */
9
+ export declare function generateSlug(name: string): string;
10
+ /**
11
+ * Generates a unique, URL-safe ID for an item based on its name and current timestamp.
12
+ *
13
+ * @param prefix - The prefix string name to add.
14
+ * @returns A unique ID string prefixed.
15
+ */
16
+ export declare function autogenerateID(prefix?: string): string;
17
+ export declare function isValidYYYYMM(month: string): month is YYYYMM;
18
+ export declare function createZeroValueRatio(currencyCode: string): ValueRatio;
19
+ export declare function createEmptySubscriptionFinance(currencyCode: string): SubscriptionFinance;
20
+ export declare function isoToYYYYMM(isoDate: string): YYYYMM;
21
+ export declare function yyyyMMToISO(yyyyMM: YYYYMM): string;
22
+ export declare function dateToYYYYMM(date: Date): YYYYMM;
23
+ export declare function yyyyMMToDate(yyyyMM: YYYYMM): Date;
24
+ export declare function offsetMonthsToYYYYMM(input: StartDateInput, offsetMonths: number): YYYYMM;
25
+ export declare function monthsInPeriod(earlier: YYYYMM, later: YYYYMM): number;
26
+ export declare function normalizeStartDate(input?: StartDateInput): Date;
package/dist/utils.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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");
17
+ /**
18
+ * Generates a URL-friendly slug from the name,
19
+ * appending a Unix timestamp to ensure uniqueness.
20
+ *
21
+ * @param name - The name to base the slug on.
22
+ * @returns A unique slug string.
23
+ */
24
+ function generateSlug(name) {
25
+ // Create the generator function ONCE with your custom alphabet and length
26
+ const generateSlugID = (0, nanoid_1.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6);
27
+ const genUID = generateSlugID(); // e.g., 'X4tF9z'
28
+ const slugText = name
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]+/g, "-")
31
+ .replace(/^-+|-+$/g, "");
32
+ return `${slugText}-${genUID}`;
33
+ }
34
+ /**
35
+ * Generates a unique, URL-safe ID for an item based on its name and current timestamp.
36
+ *
37
+ * @param prefix - The prefix string name to add.
38
+ * @returns A unique ID string prefixed.
39
+ */
40
+ function autogenerateID(prefix = "majik") {
41
+ // Create the generator function ONCE with your custom alphabet and length
42
+ const generateID = (0, nanoid_1.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 8);
43
+ // Call the generator function to produce the actual ID string
44
+ const genUID = generateID(); // Example output: 'G7K2aZp9'
45
+ return `${prefix}-${genUID}`;
46
+ }
47
+ function isValidYYYYMM(month) {
48
+ return /^\d{4}-0[1-9]|1[0-2]$/.test(month);
49
+ }
50
+ function createZeroValueRatio(currencyCode) {
51
+ const zero = majik_money_1.MajikMoney.fromMinor(0, currencyCode);
52
+ return {
53
+ value: zero,
54
+ marginRatio: 0,
55
+ };
56
+ }
57
+ function createEmptySubscriptionFinance(currencyCode) {
58
+ return {
59
+ revenue: {
60
+ gross: createZeroValueRatio(currencyCode),
61
+ net: createZeroValueRatio(currencyCode),
62
+ },
63
+ income: {
64
+ gross: createZeroValueRatio(currencyCode),
65
+ net: createZeroValueRatio(currencyCode),
66
+ },
67
+ profit: {
68
+ gross: createZeroValueRatio(currencyCode),
69
+ net: createZeroValueRatio(currencyCode),
70
+ },
71
+ cos: {
72
+ gross: createZeroValueRatio(currencyCode),
73
+ net: createZeroValueRatio(currencyCode),
74
+ },
75
+ };
76
+ }
77
+ function isoToYYYYMM(isoDate) {
78
+ const date = new Date(isoDate);
79
+ if (isNaN(date.getTime())) {
80
+ throw new Error("Invalid ISO date");
81
+ }
82
+ // Use UTC to avoid date shifting backwards/forwards based on timezone
83
+ const year = date.getUTCFullYear();
84
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
85
+ return `${year}-${month}`;
86
+ }
87
+ function yyyyMMToISO(yyyyMM) {
88
+ const [year, month] = yyyyMM.split("-").map(Number);
89
+ // Construct as UTC Midnight
90
+ return new Date(Date.UTC(year, month - 1, 1)).toISOString();
91
+ }
92
+ function dateToYYYYMM(date) {
93
+ if (isNaN(date.getTime())) {
94
+ throw new Error("Invalid Date object");
95
+ }
96
+ const year = date.getUTCFullYear();
97
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
98
+ return `${year}-${month}`;
99
+ }
100
+ function yyyyMMToDate(yyyyMM) {
101
+ const [year, month] = yyyyMM.split("-").map(Number);
102
+ return new Date(Date.UTC(year, month - 1, 1));
103
+ }
104
+ function offsetMonthsToYYYYMM(input, offsetMonths) {
105
+ const date = normalizeStartDate(input);
106
+ // Since normalizeStartDate now returns a UTC date,
107
+ // we can safely use UTC methods here.
108
+ const year = date.getUTCFullYear();
109
+ const month = date.getUTCMonth() + offsetMonths;
110
+ const result = new Date(Date.UTC(year, month, 1));
111
+ return dateToYYYYMM(result);
112
+ }
113
+ function monthsInPeriod(earlier, later) {
114
+ const start = yyyyMMToDate(earlier);
115
+ const end = yyyyMMToDate(later);
116
+ const startYear = start.getUTCFullYear();
117
+ const startMonth = start.getUTCMonth();
118
+ const endYear = end.getUTCFullYear();
119
+ const endMonth = end.getUTCMonth();
120
+ return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
121
+ }
122
+ function normalizeStartDate(input) {
123
+ if (!input) {
124
+ const now = new Date();
125
+ // Return UTC version of the first of the current month
126
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
127
+ }
128
+ // YYYYMM (e.g. "2025-03")
129
+ if (typeof input === "string" && /^\d{4}-\d{2}$/.test(input)) {
130
+ const [yyyy, mm] = input.split("-").map(Number);
131
+ // FIX: Use Date.UTC instead of new Date(yyyy, mm-1, 1)
132
+ return new Date(Date.UTC(yyyy, mm - 1, 1));
133
+ }
134
+ // ISO string
135
+ if (typeof input === "string") {
136
+ const date = new Date(input);
137
+ if (!isNaN(date.getTime())) {
138
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
139
+ }
140
+ }
141
+ // Date instance
142
+ if (input instanceof Date && !isNaN(input.getTime())) {
143
+ return new Date(Date.UTC(input.getUTCFullYear(), input.getUTCMonth(), 1));
144
+ }
145
+ throw new Error("Invalid startDate format");
146
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@thezelijah/majik-subscription",
3
+ "version": "1.0.0",
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
+ "license": "ISC",
6
+ "author": "Zelijah",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/jedlsf/majik-subscription.git"
17
+ },
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
+ "homepage": "https://github.com/jedlsf/majik-subscription#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/jedlsf/majik-subscription/issues"
45
+ },
46
+ "scripts": {
47
+ "test": "echo \"Error: no test specified\" && exit 1",
48
+ "build": "tsc",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "dependencies": {
52
+ "@thezelijah/majik-money": "^1.0.3",
53
+ "nanoid": "^5.1.6",
54
+ "typescript": "^5.9.3"
55
+ }
56
+ }