@misterhomer1992/payment-manager 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.
- package/.editorconfig +7 -0
- package/.prettierrc +5 -0
- package/README.md +181 -0
- package/USAGE.md +400 -0
- package/dist/collections/payments.d.ts +6 -0
- package/dist/collections/payments.js +60 -0
- package/dist/collections/plans.d.ts +6 -0
- package/dist/collections/plans.js +50 -0
- package/dist/collections/subscriptions.d.ts +6 -0
- package/dist/collections/subscriptions.js +47 -0
- package/dist/collections/tokenPacks.d.ts +6 -0
- package/dist/collections/tokenPacks.js +42 -0
- package/dist/collections/userTokenBalances.d.ts +4 -0
- package/dist/collections/userTokenBalances.js +41 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +14 -0
- package/dist/constants/defaultPlans.d.ts +2 -0
- package/dist/constants/defaultPlans.js +37 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +88 -0
- package/dist/services/paymentService.d.ts +8 -0
- package/dist/services/paymentService.js +173 -0
- package/dist/services/planService.d.ts +7 -0
- package/dist/services/planService.js +69 -0
- package/dist/services/subscriptionService.d.ts +13 -0
- package/dist/services/subscriptionService.js +183 -0
- package/dist/services/tokenPackService.d.ts +15 -0
- package/dist/services/tokenPackService.js +80 -0
- package/dist/services/webhookService.d.ts +3 -0
- package/dist/services/webhookService.js +210 -0
- package/dist/types/errors.d.ts +31 -0
- package/dist/types/errors.js +74 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +14 -0
- package/dist/types/orderReference.d.ts +18 -0
- package/dist/types/orderReference.js +2 -0
- package/dist/types/payment.d.ts +14 -0
- package/dist/types/payment.js +2 -0
- package/dist/types/plan.d.ts +10 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/subscription.d.ts +12 -0
- package/dist/types/subscription.js +2 -0
- package/dist/types/tokenPack.d.ts +15 -0
- package/dist/types/tokenPack.js +2 -0
- package/dist/types/webhook.d.ts +32 -0
- package/dist/types/webhook.js +2 -0
- package/dist/utils/gracePeriod.d.ts +3 -0
- package/dist/utils/gracePeriod.js +19 -0
- package/dist/utils/orderReference.d.ts +3 -0
- package/dist/utils/orderReference.js +52 -0
- package/dist/utils/proration.d.ts +10 -0
- package/dist/utils/proration.js +32 -0
- package/dist/utils/webhookResponse.d.ts +2 -0
- package/dist/utils/webhookResponse.js +10 -0
- package/jest.config.ts +11 -0
- package/package.json +29 -0
- package/src/collections/payments.ts +61 -0
- package/src/collections/plans.ts +53 -0
- package/src/collections/subscriptions.ts +52 -0
- package/src/collections/tokenPacks.ts +42 -0
- package/src/collections/userTokenBalances.ts +46 -0
- package/src/config.ts +34 -0
- package/src/index.ts +129 -0
- package/src/services/deactivationCheckHandler.test.ts +248 -0
- package/src/services/paymentCheckHandler.test.ts +384 -0
- package/src/services/paymentService.ts +166 -0
- package/src/services/planService.ts +46 -0
- package/src/services/subscriptionService.ts +183 -0
- package/src/services/tokenPackService.ts +54 -0
- package/src/services/webhookService.ts +217 -0
- package/src/types/errors.ts +72 -0
- package/src/types/index.ts +18 -0
- package/src/types/orderReference.ts +19 -0
- package/src/types/payment.ts +14 -0
- package/src/types/plan.ts +10 -0
- package/src/types/subscription.ts +12 -0
- package/src/types/tokenPack.ts +16 -0
- package/src/types/webhook.ts +34 -0
- package/src/utils/gracePeriod.test.ts +46 -0
- package/src/utils/gracePeriod.ts +19 -0
- package/src/utils/orderReference.test.ts +149 -0
- package/src/utils/orderReference.ts +57 -0
- package/src/utils/proration.test.ts +133 -0
- package/src/utils/proration.ts +50 -0
- package/src/utils/webhookResponse.test.ts +57 -0
- package/src/utils/webhookResponse.ts +14 -0
- package/tasks/prd-payment-manager-library-product-requirements-document.md +447 -0
- package/tasks/prd.json +336 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidOrderReferenceError = exports.PaymentProviderError = exports.InvalidWebhookSecretError = exports.TokenPackInactiveError = exports.TokenPackNotFoundError = exports.SubscriptionNotFoundError = exports.SubscriptionAlreadyActiveError = exports.PlanInactiveError = exports.PlanNotFoundError = exports.PaymentManagerError = void 0;
|
|
4
|
+
class PaymentManagerError extends Error {
|
|
5
|
+
constructor(message, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PaymentManagerError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.PaymentManagerError = PaymentManagerError;
|
|
12
|
+
class PlanNotFoundError extends PaymentManagerError {
|
|
13
|
+
constructor(planId) {
|
|
14
|
+
super(`Plan not found: ${planId}`, 'PLAN_NOT_FOUND');
|
|
15
|
+
this.name = 'PlanNotFoundError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.PlanNotFoundError = PlanNotFoundError;
|
|
19
|
+
class PlanInactiveError extends PaymentManagerError {
|
|
20
|
+
constructor(planId) {
|
|
21
|
+
super(`Plan is inactive: ${planId}`, 'PLAN_INACTIVE');
|
|
22
|
+
this.name = 'PlanInactiveError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.PlanInactiveError = PlanInactiveError;
|
|
26
|
+
class SubscriptionAlreadyActiveError extends PaymentManagerError {
|
|
27
|
+
constructor(userId) {
|
|
28
|
+
super(`User already has an active subscription: ${userId}`, 'SUBSCRIPTION_ALREADY_ACTIVE');
|
|
29
|
+
this.name = 'SubscriptionAlreadyActiveError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.SubscriptionAlreadyActiveError = SubscriptionAlreadyActiveError;
|
|
33
|
+
class SubscriptionNotFoundError extends PaymentManagerError {
|
|
34
|
+
constructor(userId) {
|
|
35
|
+
super(`No active subscription found for user: ${userId}`, 'SUBSCRIPTION_NOT_FOUND');
|
|
36
|
+
this.name = 'SubscriptionNotFoundError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.SubscriptionNotFoundError = SubscriptionNotFoundError;
|
|
40
|
+
class TokenPackNotFoundError extends PaymentManagerError {
|
|
41
|
+
constructor(packId) {
|
|
42
|
+
super(`Token pack not found: ${packId}`, 'TOKEN_PACK_NOT_FOUND');
|
|
43
|
+
this.name = 'TokenPackNotFoundError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.TokenPackNotFoundError = TokenPackNotFoundError;
|
|
47
|
+
class TokenPackInactiveError extends PaymentManagerError {
|
|
48
|
+
constructor(packId) {
|
|
49
|
+
super(`Token pack is inactive: ${packId}`, 'TOKEN_PACK_INACTIVE');
|
|
50
|
+
this.name = 'TokenPackInactiveError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.TokenPackInactiveError = TokenPackInactiveError;
|
|
54
|
+
class InvalidWebhookSecretError extends PaymentManagerError {
|
|
55
|
+
constructor() {
|
|
56
|
+
super('Invalid webhook secret', 'INVALID_WEBHOOK_SECRET');
|
|
57
|
+
this.name = 'InvalidWebhookSecretError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.InvalidWebhookSecretError = InvalidWebhookSecretError;
|
|
61
|
+
class PaymentProviderError extends PaymentManagerError {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message, 'PAYMENT_PROVIDER_ERROR');
|
|
64
|
+
this.name = 'PaymentProviderError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.PaymentProviderError = PaymentProviderError;
|
|
68
|
+
class InvalidOrderReferenceError extends PaymentManagerError {
|
|
69
|
+
constructor(orderReference) {
|
|
70
|
+
super(`Invalid order reference: ${orderReference}`, 'INVALID_ORDER_REFERENCE');
|
|
71
|
+
this.name = 'InvalidOrderReferenceError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.InvalidOrderReferenceError = InvalidOrderReferenceError;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SubscriptionPlanEntity } from './plan.js';
|
|
2
|
+
export { SubscriptionEntity } from './subscription.js';
|
|
3
|
+
export { PaymentEntity } from './payment.js';
|
|
4
|
+
export { TokenPackEntity, UserTokenBalance } from './tokenPack.js';
|
|
5
|
+
export { WebhookResponse, DeactivationResult, ServiceUrlRequestParams } from './webhook.js';
|
|
6
|
+
export { OrderReferenceParams, ParsedOrderReference } from './orderReference.js';
|
|
7
|
+
export { PaymentManagerError, PlanNotFoundError, PlanInactiveError, SubscriptionAlreadyActiveError, SubscriptionNotFoundError, TokenPackNotFoundError, TokenPackInactiveError, InvalidWebhookSecretError, PaymentProviderError, InvalidOrderReferenceError, } from './errors.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidOrderReferenceError = exports.PaymentProviderError = exports.InvalidWebhookSecretError = exports.TokenPackInactiveError = exports.TokenPackNotFoundError = exports.SubscriptionNotFoundError = exports.SubscriptionAlreadyActiveError = exports.PlanInactiveError = exports.PlanNotFoundError = exports.PaymentManagerError = void 0;
|
|
4
|
+
var errors_js_1 = require("./errors.js");
|
|
5
|
+
Object.defineProperty(exports, "PaymentManagerError", { enumerable: true, get: function () { return errors_js_1.PaymentManagerError; } });
|
|
6
|
+
Object.defineProperty(exports, "PlanNotFoundError", { enumerable: true, get: function () { return errors_js_1.PlanNotFoundError; } });
|
|
7
|
+
Object.defineProperty(exports, "PlanInactiveError", { enumerable: true, get: function () { return errors_js_1.PlanInactiveError; } });
|
|
8
|
+
Object.defineProperty(exports, "SubscriptionAlreadyActiveError", { enumerable: true, get: function () { return errors_js_1.SubscriptionAlreadyActiveError; } });
|
|
9
|
+
Object.defineProperty(exports, "SubscriptionNotFoundError", { enumerable: true, get: function () { return errors_js_1.SubscriptionNotFoundError; } });
|
|
10
|
+
Object.defineProperty(exports, "TokenPackNotFoundError", { enumerable: true, get: function () { return errors_js_1.TokenPackNotFoundError; } });
|
|
11
|
+
Object.defineProperty(exports, "TokenPackInactiveError", { enumerable: true, get: function () { return errors_js_1.TokenPackInactiveError; } });
|
|
12
|
+
Object.defineProperty(exports, "InvalidWebhookSecretError", { enumerable: true, get: function () { return errors_js_1.InvalidWebhookSecretError; } });
|
|
13
|
+
Object.defineProperty(exports, "PaymentProviderError", { enumerable: true, get: function () { return errors_js_1.PaymentProviderError; } });
|
|
14
|
+
Object.defineProperty(exports, "InvalidOrderReferenceError", { enumerable: true, get: function () { return errors_js_1.InvalidOrderReferenceError; } });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface OrderReferenceParams {
|
|
2
|
+
version: string;
|
|
3
|
+
platform: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
type: 'sub' | 'tkn';
|
|
6
|
+
planId?: string;
|
|
7
|
+
tokenPackId?: string;
|
|
8
|
+
date: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface ParsedOrderReference {
|
|
11
|
+
version: string;
|
|
12
|
+
platform: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
type: 'sub' | 'tkn';
|
|
15
|
+
planId?: string;
|
|
16
|
+
tokenPackId?: string;
|
|
17
|
+
date: Date;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PaymentEntity {
|
|
2
|
+
id: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
planId?: string;
|
|
5
|
+
tokenPackId?: string;
|
|
6
|
+
amount: number;
|
|
7
|
+
currency: string;
|
|
8
|
+
status: 'pending' | 'approved' | 'rejected';
|
|
9
|
+
orderReference: string;
|
|
10
|
+
paymentLink?: string;
|
|
11
|
+
paymentLinkCreatedAt?: Date;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
updatedAt: Date;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SubscriptionEntity {
|
|
2
|
+
id: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
planId: string;
|
|
5
|
+
status: 'active' | 'cancelled' | 'expired';
|
|
6
|
+
startedAt: Date;
|
|
7
|
+
expiresAt: Date;
|
|
8
|
+
pendingPlanId?: string;
|
|
9
|
+
pendingPlanChangeDate?: Date;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface TokenPackEntity {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
tokenAmount: number;
|
|
5
|
+
price: number;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
updatedAt: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface UserTokenBalance {
|
|
11
|
+
userId: string;
|
|
12
|
+
balance: number;
|
|
13
|
+
totalPurchased: number;
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface WebhookResponse {
|
|
2
|
+
orderReference: string;
|
|
3
|
+
status: 'accept' | 'reject';
|
|
4
|
+
time: string;
|
|
5
|
+
signature: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DeactivationResult {
|
|
8
|
+
expired: string[];
|
|
9
|
+
downgraded: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ServiceUrlRequestParams {
|
|
12
|
+
merchantAccount: string;
|
|
13
|
+
orderReference: string;
|
|
14
|
+
merchantSignature: string;
|
|
15
|
+
amount: number;
|
|
16
|
+
currency: string;
|
|
17
|
+
authCode: string;
|
|
18
|
+
email: string;
|
|
19
|
+
phone: string;
|
|
20
|
+
createdDate: number;
|
|
21
|
+
processingDate: number;
|
|
22
|
+
cardPan: string;
|
|
23
|
+
cardType: string;
|
|
24
|
+
issuerBankCountry: string;
|
|
25
|
+
issuerBankName: string;
|
|
26
|
+
recToken: string;
|
|
27
|
+
transactionStatus: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
reasonCode: number;
|
|
30
|
+
fee: number;
|
|
31
|
+
paymentSystem: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SubscriptionPlanEntity } from '../types/plan.js';
|
|
2
|
+
export declare function getGracePeriod(regularMode: SubscriptionPlanEntity['regularMode']): number;
|
|
3
|
+
export declare function getExpirationWithGrace(expiresAt: Date, regularMode: SubscriptionPlanEntity['regularMode']): Date;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getGracePeriod = getGracePeriod;
|
|
4
|
+
exports.getExpirationWithGrace = getExpirationWithGrace;
|
|
5
|
+
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
6
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
7
|
+
function getGracePeriod(regularMode) {
|
|
8
|
+
switch (regularMode) {
|
|
9
|
+
case 'daily':
|
|
10
|
+
return 6 * MS_PER_HOUR;
|
|
11
|
+
case 'monthly':
|
|
12
|
+
return MS_PER_DAY;
|
|
13
|
+
case 'yearly':
|
|
14
|
+
return MS_PER_DAY;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getExpirationWithGrace(expiresAt, regularMode) {
|
|
18
|
+
return new Date(expiresAt.getTime() + getGracePeriod(regularMode));
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOrderReference = buildOrderReference;
|
|
4
|
+
exports.parseOrderReference = parseOrderReference;
|
|
5
|
+
const errors_js_1 = require("../types/errors.js");
|
|
6
|
+
const PREFIX = 'miiabot';
|
|
7
|
+
function buildOrderReference(params) {
|
|
8
|
+
const { version, platform, userId, type, planId, tokenPackId, date } = params;
|
|
9
|
+
const idSegment = type === 'sub' ? `plnId=${planId}` : `tknId=${tokenPackId}`;
|
|
10
|
+
const utcDate = date.toISOString();
|
|
11
|
+
return `${PREFIX}_v=${version}_p=${platform}_uid=${userId}_type=${type}_${idSegment}_utcDate=${utcDate}`;
|
|
12
|
+
}
|
|
13
|
+
function parseOrderReference(ref) {
|
|
14
|
+
// Strip _WFPREG-* suffix added by WayForPay for recurrent payments
|
|
15
|
+
const cleaned = ref.replace(/_WFPREG-.*$/, '');
|
|
16
|
+
const vMatch = cleaned.match(/_v=([^_]+)/);
|
|
17
|
+
const pMatch = cleaned.match(/_p=([^_]+)/);
|
|
18
|
+
const uidMatch = cleaned.match(/_uid=([^_]+)/);
|
|
19
|
+
const typeMatch = cleaned.match(/_type=(sub|tkn)/);
|
|
20
|
+
const dateMatch = cleaned.match(/_utcDate=(.+)$/);
|
|
21
|
+
if (!vMatch || !pMatch || !uidMatch || !typeMatch || !dateMatch) {
|
|
22
|
+
throw new errors_js_1.InvalidOrderReferenceError(ref);
|
|
23
|
+
}
|
|
24
|
+
if (!cleaned.startsWith(PREFIX + '_')) {
|
|
25
|
+
throw new errors_js_1.InvalidOrderReferenceError(ref);
|
|
26
|
+
}
|
|
27
|
+
const type = typeMatch[1];
|
|
28
|
+
const parsedDate = new Date(dateMatch[1]);
|
|
29
|
+
if (isNaN(parsedDate.getTime())) {
|
|
30
|
+
throw new errors_js_1.InvalidOrderReferenceError(ref);
|
|
31
|
+
}
|
|
32
|
+
const result = {
|
|
33
|
+
version: vMatch[1],
|
|
34
|
+
platform: pMatch[1],
|
|
35
|
+
userId: uidMatch[1],
|
|
36
|
+
type,
|
|
37
|
+
date: parsedDate,
|
|
38
|
+
};
|
|
39
|
+
if (type === 'sub') {
|
|
40
|
+
const plnMatch = cleaned.match(/_plnId=([^_]+)/);
|
|
41
|
+
if (!plnMatch)
|
|
42
|
+
throw new errors_js_1.InvalidOrderReferenceError(ref);
|
|
43
|
+
result.planId = plnMatch[1];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const tknMatch = cleaned.match(/_tknId=([^_]+)/);
|
|
47
|
+
if (!tknMatch)
|
|
48
|
+
throw new errors_js_1.InvalidOrderReferenceError(ref);
|
|
49
|
+
result.tokenPackId = tknMatch[1];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SubscriptionPlanEntity } from '../types/plan.js';
|
|
2
|
+
export declare function cycleDays(regularMode: SubscriptionPlanEntity['regularMode']): number;
|
|
3
|
+
export interface ProrationResult {
|
|
4
|
+
remainingRatio: number;
|
|
5
|
+
remainingValue: number;
|
|
6
|
+
newDailyRate: number;
|
|
7
|
+
daysOnNewPlan: number;
|
|
8
|
+
newExpiresAt: Date;
|
|
9
|
+
}
|
|
10
|
+
export declare function calculateUpgradeProration(currentPlan: SubscriptionPlanEntity, newPlan: SubscriptionPlanEntity, startedAt: Date, expiresAt: Date, now: Date): ProrationResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cycleDays = cycleDays;
|
|
4
|
+
exports.calculateUpgradeProration = calculateUpgradeProration;
|
|
5
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
6
|
+
function cycleDays(regularMode) {
|
|
7
|
+
switch (regularMode) {
|
|
8
|
+
case 'daily':
|
|
9
|
+
return 1;
|
|
10
|
+
case 'monthly':
|
|
11
|
+
return 30;
|
|
12
|
+
case 'yearly':
|
|
13
|
+
return 365;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function calculateUpgradeProration(currentPlan, newPlan, startedAt, expiresAt, now) {
|
|
17
|
+
const totalDays = (expiresAt.getTime() - startedAt.getTime()) / MS_PER_DAY;
|
|
18
|
+
const daysUsed = (now.getTime() - startedAt.getTime()) / MS_PER_DAY;
|
|
19
|
+
const remainingDays = totalDays - daysUsed;
|
|
20
|
+
const remainingRatio = remainingDays / totalDays;
|
|
21
|
+
const remainingValue = remainingRatio * currentPlan.amount;
|
|
22
|
+
const newDailyRate = newPlan.amount / cycleDays(newPlan.regularMode);
|
|
23
|
+
const daysOnNewPlan = remainingValue / newDailyRate;
|
|
24
|
+
const newExpiresAt = new Date(now.getTime() + daysOnNewPlan * MS_PER_DAY);
|
|
25
|
+
return {
|
|
26
|
+
remainingRatio,
|
|
27
|
+
remainingValue,
|
|
28
|
+
newDailyRate,
|
|
29
|
+
daysOnNewPlan,
|
|
30
|
+
newExpiresAt,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildWebhookResponse = buildWebhookResponse;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
function buildWebhookResponse(orderReference, status, merchantSecretKey) {
|
|
6
|
+
const time = new Date().toISOString();
|
|
7
|
+
const data = `${orderReference};${status};${time}`;
|
|
8
|
+
const signature = (0, node_crypto_1.createHmac)('md5', merchantSecretKey).update(data).digest('hex');
|
|
9
|
+
return { orderReference, status, time, signature };
|
|
10
|
+
}
|
package/jest.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@misterhomer1992/payment-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Payment management library with WayForPay and Firebase integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --passWithNoTests",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\""
|
|
12
|
+
},
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@misterhomer1992/wayforpay-api": "^3.0.1"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"firebase-admin": ">=12.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/jest": "^29.5.14",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"prettier": "^3.8.1",
|
|
25
|
+
"ts-jest": "^29.2.5",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"typescript": "^5.7.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getConfig } from '../config.js';
|
|
2
|
+
import type { PaymentEntity } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
const COLLECTION = 'payments';
|
|
5
|
+
|
|
6
|
+
function collection() {
|
|
7
|
+
return getConfig().firestore.collection(COLLECTION);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function create(payment: Omit<PaymentEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const docRef = await collection().add({
|
|
13
|
+
...payment,
|
|
14
|
+
createdAt: now,
|
|
15
|
+
updatedAt: now,
|
|
16
|
+
});
|
|
17
|
+
return docRef.id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function update(paymentId: string, data: Partial<Omit<PaymentEntity, 'id'>>): Promise<void> {
|
|
21
|
+
await collection()
|
|
22
|
+
.doc(paymentId)
|
|
23
|
+
.update({
|
|
24
|
+
...data,
|
|
25
|
+
updatedAt: new Date(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function findPendingByUserAndPlan(userId: string, planId: string): Promise<PaymentEntity | null> {
|
|
30
|
+
const snapshot = await collection()
|
|
31
|
+
.where('userId', '==', userId)
|
|
32
|
+
.where('planId', '==', planId)
|
|
33
|
+
.where('status', '==', 'pending')
|
|
34
|
+
.limit(1)
|
|
35
|
+
.get();
|
|
36
|
+
|
|
37
|
+
if (snapshot.empty) return null;
|
|
38
|
+
const doc = snapshot.docs[0];
|
|
39
|
+
return { id: doc.id, ...doc.data() } as PaymentEntity;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function findPendingByUserAndTokenPack(userId: string, packId: string): Promise<PaymentEntity | null> {
|
|
43
|
+
const snapshot = await collection()
|
|
44
|
+
.where('userId', '==', userId)
|
|
45
|
+
.where('tokenPackId', '==', packId)
|
|
46
|
+
.where('status', '==', 'pending')
|
|
47
|
+
.limit(1)
|
|
48
|
+
.get();
|
|
49
|
+
|
|
50
|
+
if (snapshot.empty) return null;
|
|
51
|
+
const doc = snapshot.docs[0];
|
|
52
|
+
return { id: doc.id, ...doc.data() } as PaymentEntity;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function findByOrderReference(orderReference: string): Promise<PaymentEntity | null> {
|
|
56
|
+
const snapshot = await collection().where('orderReference', '==', orderReference).limit(1).get();
|
|
57
|
+
|
|
58
|
+
if (snapshot.empty) return null;
|
|
59
|
+
const doc = snapshot.docs[0];
|
|
60
|
+
return { id: doc.id, ...doc.data() } as PaymentEntity;
|
|
61
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getConfig } from '../config.js';
|
|
2
|
+
import type { SubscriptionPlanEntity } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
const COLLECTION = 'subscriptionPlans';
|
|
5
|
+
|
|
6
|
+
function collection() {
|
|
7
|
+
return getConfig().firestore.collection(COLLECTION);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getById(planId: string): Promise<SubscriptionPlanEntity | null> {
|
|
11
|
+
const doc = await collection().doc(planId).get();
|
|
12
|
+
if (!doc.exists) return null;
|
|
13
|
+
return { id: doc.id, ...doc.data() } as SubscriptionPlanEntity;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getAll(): Promise<SubscriptionPlanEntity[]> {
|
|
17
|
+
const snapshot = await collection().get();
|
|
18
|
+
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as SubscriptionPlanEntity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function create(plan: Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>): Promise<void> {
|
|
22
|
+
const now = new Date();
|
|
23
|
+
await collection()
|
|
24
|
+
.doc(plan.id)
|
|
25
|
+
.set({
|
|
26
|
+
...plan,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function update(planId: string, data: Partial<Omit<SubscriptionPlanEntity, 'id'>>): Promise<void> {
|
|
33
|
+
await collection()
|
|
34
|
+
.doc(planId)
|
|
35
|
+
.update({
|
|
36
|
+
...data,
|
|
37
|
+
updatedAt: new Date(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function upsert(plan: Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>): Promise<void> {
|
|
42
|
+
const now = new Date();
|
|
43
|
+
await collection()
|
|
44
|
+
.doc(plan.id)
|
|
45
|
+
.set(
|
|
46
|
+
{
|
|
47
|
+
...plan,
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
},
|
|
51
|
+
{ merge: true },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getConfig } from '../config.js';
|
|
2
|
+
import type { SubscriptionEntity } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
const COLLECTION = 'subscriptions';
|
|
5
|
+
|
|
6
|
+
function collection() {
|
|
7
|
+
return getConfig().firestore.collection(COLLECTION);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getActiveByUserId(userId: string): Promise<SubscriptionEntity | null> {
|
|
11
|
+
const snapshot = await collection().where('userId', '==', userId).where('status', '==', 'active').limit(1).get();
|
|
12
|
+
|
|
13
|
+
if (snapshot.empty) return null;
|
|
14
|
+
const doc = snapshot.docs[0];
|
|
15
|
+
return { id: doc.id, ...doc.data() } as SubscriptionEntity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getByUserId(userId: string): Promise<SubscriptionEntity[]> {
|
|
19
|
+
const snapshot = await collection()
|
|
20
|
+
.where('userId', '==', userId)
|
|
21
|
+
.where('status', 'in', ['active', 'cancelled'])
|
|
22
|
+
.get();
|
|
23
|
+
|
|
24
|
+
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as SubscriptionEntity);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function create(
|
|
28
|
+
subscription: Omit<SubscriptionEntity, 'id' | 'createdAt' | 'updatedAt'>,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const docRef = await collection().add({
|
|
32
|
+
...subscription,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
});
|
|
36
|
+
return docRef.id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function update(subscriptionId: string, data: Partial<Omit<SubscriptionEntity, 'id'>>): Promise<void> {
|
|
40
|
+
await collection()
|
|
41
|
+
.doc(subscriptionId)
|
|
42
|
+
.update({
|
|
43
|
+
...data,
|
|
44
|
+
updatedAt: new Date(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getAllActiveAndCancelled(): Promise<SubscriptionEntity[]> {
|
|
49
|
+
const snapshot = await collection().where('status', 'in', ['active', 'cancelled']).get();
|
|
50
|
+
|
|
51
|
+
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as SubscriptionEntity);
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getConfig } from '../config.js';
|
|
2
|
+
import type { TokenPackEntity } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
const COLLECTION = 'tokenPacks';
|
|
5
|
+
|
|
6
|
+
function collection() {
|
|
7
|
+
return getConfig().firestore.collection(COLLECTION);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getById(packId: string): Promise<TokenPackEntity | null> {
|
|
11
|
+
const doc = await collection().doc(packId).get();
|
|
12
|
+
if (!doc.exists) return null;
|
|
13
|
+
return { id: doc.id, ...doc.data() } as TokenPackEntity;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getAll(): Promise<TokenPackEntity[]> {
|
|
17
|
+
const snapshot = await collection().get();
|
|
18
|
+
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TokenPackEntity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function create(pack: Omit<TokenPackEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> {
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const docRef = await collection().add({
|
|
24
|
+
...pack,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
});
|
|
28
|
+
return docRef.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function update(packId: string, data: Partial<Omit<TokenPackEntity, 'id'>>): Promise<void> {
|
|
32
|
+
await collection()
|
|
33
|
+
.doc(packId)
|
|
34
|
+
.update({
|
|
35
|
+
...data,
|
|
36
|
+
updatedAt: new Date(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function remove(packId: string): Promise<void> {
|
|
41
|
+
await collection().doc(packId).delete();
|
|
42
|
+
}
|