payment-kit 1.13.65 → 1.13.67
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/api/src/index.ts +4 -0
- package/api/src/integrations/blocklet/notification.ts +5 -3
- package/api/src/jobs/notification.ts +142 -0
- package/api/src/jobs/payment.ts +14 -0
- package/api/src/jobs/subscription.ts +2 -2
- package/api/src/libs/audit.ts +3 -1
- package/api/src/libs/env.ts +3 -0
- package/api/src/libs/event.ts +10 -1
- package/api/src/libs/invoice.ts +5 -0
- package/api/src/libs/notification/index.ts +23 -0
- package/api/src/libs/notification/template/base.ts +12 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +286 -0
- package/api/src/libs/notification/template/subscription-renewed.ts +259 -0
- package/api/src/libs/notification/template/subscription-succeeded.ts +279 -0
- package/api/src/libs/notification/template/subscription-trial-start.ts +267 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +250 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +232 -0
- package/api/src/libs/payment.ts +100 -3
- package/api/src/libs/product.ts +19 -0
- package/api/src/libs/queue/index.ts +13 -0
- package/api/src/libs/subscription.ts +5 -0
- package/api/src/libs/time.ts +17 -0
- package/api/src/libs/util.ts +39 -0
- package/api/src/locales/en.ts +67 -0
- package/api/src/locales/zh.ts +64 -0
- package/api/src/routes/connect/collect.ts +6 -0
- package/api/src/schedule/index.ts +28 -0
- package/api/src/schedule/interface/diff.ts +9 -0
- package/api/src/schedule/subscription-trail-will-end.ts +197 -0
- package/api/src/schedule/subscription-will-renew.ts +195 -0
- package/api/src/store/models/subscription.ts +30 -12
- package/api/src/store/models/types.ts +13 -12
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -7
- package/src/app.tsx +2 -0
- package/src/components/invoice/action.tsx +25 -7
- package/src/components/invoice/list.tsx +19 -4
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/list.tsx +6 -5
- package/src/components/subscription/items/index.tsx +8 -4
- package/src/libs/util.ts +2 -2
- package/src/locales/en.tsx +5 -1
- package/src/locales/zh.tsx +5 -1
- package/src/pages/checkout/pricing-table.tsx +1 -1
- package/src/pages/customer/index.tsx +13 -2
- package/src/pages/customer/invoice.tsx +5 -4
- package/src/pages/customer/subscription/index.tsx +163 -0
- package/tsconfig.api.json +6 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { clone } from 'lodash';
|
|
4
|
+
import pAll from 'p-all';
|
|
5
|
+
import { Op } from 'sequelize';
|
|
6
|
+
|
|
7
|
+
import { NotificationQueueJob, notificationQueue } from '../jobs/notification';
|
|
8
|
+
import { notificationCronConcurrency } from '../libs/env';
|
|
9
|
+
import logger from '../libs/logger';
|
|
10
|
+
import type { SubscriptionTrialWillEndEmailTemplateOptions } from '../libs/notification/template/subscription-trial-will-end';
|
|
11
|
+
import { Subscription } from '../store/models';
|
|
12
|
+
import type { Diff } from './interface/diff';
|
|
13
|
+
|
|
14
|
+
interface SubscriptionForTrailWillEnd extends Subscription {
|
|
15
|
+
diffs: Diff[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SubscriptionTrailWillEndSchedule {
|
|
19
|
+
private start: number;
|
|
20
|
+
|
|
21
|
+
private end: number;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.start = Date.now();
|
|
25
|
+
this.end = dayjs(this.start).add(1, 'M').add(1, 'd').toDate().getTime();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async run() {
|
|
29
|
+
const label: string = new Date().toISOString();
|
|
30
|
+
|
|
31
|
+
logger.info(`${label}: SubscriptionTrailWillEndSchedule.run start`);
|
|
32
|
+
|
|
33
|
+
const subscriptions = await this.getSubscriptions();
|
|
34
|
+
const subscriptionForWillRenew = this.getSubscriptionsForWillRenew(subscriptions);
|
|
35
|
+
|
|
36
|
+
await this.addTaskToQueue(subscriptionForWillRenew);
|
|
37
|
+
|
|
38
|
+
logger.info(`${label}: SubscriptionTrailWillEndSchedule.run end`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getSubscriptions(): Promise<Subscription[]> {
|
|
42
|
+
const subscriptions = await Subscription.findAll({
|
|
43
|
+
where: {
|
|
44
|
+
// @note: 之所以除 1000,是因为时间的精确度是 s
|
|
45
|
+
current_period_start: {
|
|
46
|
+
[Op.lte]: this.start / 1000,
|
|
47
|
+
},
|
|
48
|
+
current_period_end: {
|
|
49
|
+
[Op.lt]: this.end / 1000,
|
|
50
|
+
},
|
|
51
|
+
trail_start: {
|
|
52
|
+
[Op.gt]: 0,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
attributes: ['id', 'current_period_start', 'current_period_end'],
|
|
56
|
+
raw: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return subscriptions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static DIFFS: Diff[] = [
|
|
63
|
+
{
|
|
64
|
+
value: 1,
|
|
65
|
+
unit: 'M',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: 7,
|
|
69
|
+
unit: 'd',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
value: 3,
|
|
73
|
+
unit: 'd',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
value: 1,
|
|
77
|
+
unit: 'd',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: 6,
|
|
81
|
+
unit: 'h',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
|
|
87
|
+
* @description
|
|
88
|
+
* @param {Subscription[]} subscriptions
|
|
89
|
+
* @return {*} {SubscriptionForWillRenew[]}
|
|
90
|
+
* @memberof SubscriptionWillRenewSchedule
|
|
91
|
+
*/
|
|
92
|
+
getSubscriptionsForWillRenew(subscriptions: Subscription[]): SubscriptionForTrailWillEnd[] {
|
|
93
|
+
return subscriptions.map((subscription: Subscription): SubscriptionForTrailWillEnd => {
|
|
94
|
+
const s: SubscriptionForTrailWillEnd = clone(subscription) as SubscriptionForTrailWillEnd;
|
|
95
|
+
const currentPeriodStart: number = s.current_period_start * 1000;
|
|
96
|
+
const currentPeriodEnd: number = s.current_period_end * 1000;
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
|
|
100
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1
|
|
101
|
+
) {
|
|
102
|
+
s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(0);
|
|
103
|
+
} else if (
|
|
104
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 7 &&
|
|
105
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7
|
|
106
|
+
) {
|
|
107
|
+
s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(1);
|
|
108
|
+
} else if (
|
|
109
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 3 &&
|
|
110
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 3
|
|
111
|
+
) {
|
|
112
|
+
s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(2);
|
|
113
|
+
} else if (
|
|
114
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 1 &&
|
|
115
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 1
|
|
116
|
+
) {
|
|
117
|
+
s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(3);
|
|
118
|
+
} else if (
|
|
119
|
+
dayjs(currentPeriodEnd).diff(this.start, 'h') >= 6 &&
|
|
120
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') > 6
|
|
121
|
+
) {
|
|
122
|
+
s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(4);
|
|
123
|
+
} else {
|
|
124
|
+
s.diffs = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 每一个周期都至少收到一封邮件
|
|
128
|
+
if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1) {
|
|
129
|
+
s.diffs.forEach((x) => {
|
|
130
|
+
if (x.value === 7 && x.unit === 'd') {
|
|
131
|
+
x.required = true;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7) {
|
|
135
|
+
s.diffs.forEach((x) => {
|
|
136
|
+
if (x.value === 3 && x.unit === 'd') {
|
|
137
|
+
x.required = true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') >= 6) {
|
|
141
|
+
s.diffs.forEach((x) => {
|
|
142
|
+
if (x.value === 6 && x.unit === 'h') {
|
|
143
|
+
x.required = true;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return s;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async addTaskToQueue(subscriptions: SubscriptionForTrailWillEnd[]): Promise<void> {
|
|
153
|
+
const tasks: {
|
|
154
|
+
id: string;
|
|
155
|
+
job: { type: NotificationQueueJob['type']; options: SubscriptionTrialWillEndEmailTemplateOptions };
|
|
156
|
+
delay: number;
|
|
157
|
+
}[] = [];
|
|
158
|
+
|
|
159
|
+
for (const subscription of subscriptions) {
|
|
160
|
+
for (const diff of subscription.diffs) {
|
|
161
|
+
const id: string = `${subscription.id}.${diff.value}.${diff.unit}`;
|
|
162
|
+
const job: { type: NotificationQueueJob['type']; options: SubscriptionTrialWillEndEmailTemplateOptions } = {
|
|
163
|
+
type: 'customer.subscription.trial_will_end',
|
|
164
|
+
options: {
|
|
165
|
+
subscriptionId: subscription.id,
|
|
166
|
+
willRenewValue: diff.value,
|
|
167
|
+
willRenewUnit: diff.unit,
|
|
168
|
+
required: !!diff.required,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
const delay: number = dayjs(subscription.current_period_end * 1000)
|
|
172
|
+
.subtract(diff.value, diff.unit)
|
|
173
|
+
.diff(this.start, 's');
|
|
174
|
+
|
|
175
|
+
tasks.push({
|
|
176
|
+
id,
|
|
177
|
+
job,
|
|
178
|
+
delay,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await pAll(
|
|
184
|
+
tasks.map((x) => {
|
|
185
|
+
return async () => {
|
|
186
|
+
const job = await notificationQueue.get(x.id);
|
|
187
|
+
if (job) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
notificationQueue.push(x);
|
|
192
|
+
};
|
|
193
|
+
}),
|
|
194
|
+
{ concurrency: notificationCronConcurrency }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { clone } from 'lodash';
|
|
4
|
+
import pAll from 'p-all';
|
|
5
|
+
import { Op } from 'sequelize';
|
|
6
|
+
|
|
7
|
+
import { NotificationQueueJob, notificationQueue } from '../jobs/notification';
|
|
8
|
+
import { notificationCronConcurrency } from '../libs/env';
|
|
9
|
+
import logger from '../libs/logger';
|
|
10
|
+
import type { SubscriptionWillRenewEmailTemplateOptions } from '../libs/notification/template/subscription-will-renew';
|
|
11
|
+
import { Subscription } from '../store/models';
|
|
12
|
+
import type { Diff } from './interface/diff';
|
|
13
|
+
|
|
14
|
+
interface SubscriptionForWillRenew extends Subscription {
|
|
15
|
+
diffs: Diff[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SubscriptionWillRenewSchedule {
|
|
19
|
+
private start: number;
|
|
20
|
+
|
|
21
|
+
private end: number;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.start = Date.now();
|
|
25
|
+
this.end = dayjs(this.start).add(1, 'M').add(1, 'd').toDate().getTime();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async run() {
|
|
29
|
+
const label: string = new Date().toISOString();
|
|
30
|
+
|
|
31
|
+
logger.info(`${label}: SubscriptionWillRenewSchedule.run start`);
|
|
32
|
+
|
|
33
|
+
const subscriptions = await this.getSubscriptions();
|
|
34
|
+
const subscriptionForWillRenew = this.getSubscriptionsForWillRenew(subscriptions);
|
|
35
|
+
|
|
36
|
+
await this.addTaskToQueue(subscriptionForWillRenew);
|
|
37
|
+
|
|
38
|
+
logger.info(`${label}: SubscriptionWillRenewSchedule.run end`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getSubscriptions(): Promise<Subscription[]> {
|
|
42
|
+
const subscriptions = await Subscription.findAll({
|
|
43
|
+
where: {
|
|
44
|
+
// @note: 之所以除 1000,是因为时间的精确度是 s
|
|
45
|
+
current_period_start: {
|
|
46
|
+
[Op.lte]: this.start / 1000,
|
|
47
|
+
},
|
|
48
|
+
current_period_end: {
|
|
49
|
+
[Op.lt]: this.end / 1000,
|
|
50
|
+
},
|
|
51
|
+
trail_start: 0,
|
|
52
|
+
},
|
|
53
|
+
attributes: ['id', 'current_period_start', 'current_period_end'],
|
|
54
|
+
raw: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return subscriptions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static DIFFS: Diff[] = [
|
|
61
|
+
{
|
|
62
|
+
value: 1,
|
|
63
|
+
unit: 'M',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: 7,
|
|
67
|
+
unit: 'd',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
value: 3,
|
|
71
|
+
unit: 'd',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
value: 1,
|
|
75
|
+
unit: 'd',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
value: 6,
|
|
79
|
+
unit: 'h',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
|
|
85
|
+
* @description
|
|
86
|
+
* @param {Subscription[]} subscriptions
|
|
87
|
+
* @return {*} {SubscriptionForWillRenew[]}
|
|
88
|
+
* @memberof SubscriptionWillRenewSchedule
|
|
89
|
+
*/
|
|
90
|
+
getSubscriptionsForWillRenew(subscriptions: Subscription[]): SubscriptionForWillRenew[] {
|
|
91
|
+
return subscriptions.map((subscription: Subscription): SubscriptionForWillRenew => {
|
|
92
|
+
const s: SubscriptionForWillRenew = clone(subscription) as SubscriptionForWillRenew;
|
|
93
|
+
const currentPeriodStart: number = s.current_period_start * 1000;
|
|
94
|
+
const currentPeriodEnd: number = s.current_period_end * 1000;
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
|
|
98
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1
|
|
99
|
+
) {
|
|
100
|
+
s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(0);
|
|
101
|
+
} else if (
|
|
102
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 7 &&
|
|
103
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7
|
|
104
|
+
) {
|
|
105
|
+
s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(1);
|
|
106
|
+
} else if (
|
|
107
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 3 &&
|
|
108
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 3
|
|
109
|
+
) {
|
|
110
|
+
s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(2);
|
|
111
|
+
} else if (
|
|
112
|
+
dayjs(currentPeriodEnd).diff(this.start, 'd') >= 1 &&
|
|
113
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 1
|
|
114
|
+
) {
|
|
115
|
+
s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(3);
|
|
116
|
+
} else if (
|
|
117
|
+
dayjs(currentPeriodEnd).diff(this.start, 'h') >= 6 &&
|
|
118
|
+
dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') > 6
|
|
119
|
+
) {
|
|
120
|
+
s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(4);
|
|
121
|
+
} else {
|
|
122
|
+
s.diffs = [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 每一个周期都至少收到一封邮件
|
|
126
|
+
if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1) {
|
|
127
|
+
s.diffs.forEach((x) => {
|
|
128
|
+
if (x.value === 7 && x.unit === 'd') {
|
|
129
|
+
x.required = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
} else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7) {
|
|
133
|
+
s.diffs.forEach((x) => {
|
|
134
|
+
if (x.value === 3 && x.unit === 'd') {
|
|
135
|
+
x.required = true;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
} else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') >= 6) {
|
|
139
|
+
s.diffs.forEach((x) => {
|
|
140
|
+
if (x.value === 6 && x.unit === 'h') {
|
|
141
|
+
x.required = true;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return s;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async addTaskToQueue(subscriptions: SubscriptionForWillRenew[]): Promise<void> {
|
|
151
|
+
const tasks: {
|
|
152
|
+
id: string;
|
|
153
|
+
job: { type: NotificationQueueJob['type']; options: SubscriptionWillRenewEmailTemplateOptions };
|
|
154
|
+
delay: number;
|
|
155
|
+
}[] = [];
|
|
156
|
+
|
|
157
|
+
for (const subscription of subscriptions) {
|
|
158
|
+
for (const diff of subscription.diffs) {
|
|
159
|
+
const id: string = `${subscription.id}.${diff.value}.${diff.unit}`;
|
|
160
|
+
const job: { type: NotificationQueueJob['type']; options: SubscriptionWillRenewEmailTemplateOptions } = {
|
|
161
|
+
type: 'customer.subscription.will_renew',
|
|
162
|
+
options: {
|
|
163
|
+
subscriptionId: subscription.id,
|
|
164
|
+
willRenewValue: diff.value,
|
|
165
|
+
willRenewUnit: diff.unit,
|
|
166
|
+
required: !!diff.required,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
const delay: number = dayjs(subscription.current_period_end * 1000)
|
|
170
|
+
.subtract(diff.value, diff.unit)
|
|
171
|
+
.diff(this.start, 's');
|
|
172
|
+
|
|
173
|
+
tasks.push({
|
|
174
|
+
id,
|
|
175
|
+
job,
|
|
176
|
+
delay,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await pAll(
|
|
182
|
+
tasks.map((x) => {
|
|
183
|
+
return async () => {
|
|
184
|
+
const job = await notificationQueue.get(x.id);
|
|
185
|
+
if (job) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
notificationQueue.push(x);
|
|
190
|
+
};
|
|
191
|
+
}),
|
|
192
|
+
{ concurrency: notificationCronConcurrency }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
5
5
|
|
|
6
6
|
import { createCustomEvent, createEvent, createStatusEvent } from '../../libs/audit';
|
|
7
7
|
import { createIdGenerator } from '../../libs/util';
|
|
8
|
+
import { Invoice } from './invoice';
|
|
8
9
|
import type { PaymentDetails, PaymentSettings, PriceRecurring } from './types';
|
|
9
10
|
|
|
10
11
|
const nextId = createIdGenerator('sub', 24);
|
|
@@ -266,10 +267,36 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
266
267
|
createdAt: 'created_at',
|
|
267
268
|
updatedAt: 'updated_at',
|
|
268
269
|
hooks: {
|
|
269
|
-
afterCreate: (model: Subscription, options) =>
|
|
270
|
-
createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error)
|
|
271
|
-
|
|
270
|
+
afterCreate: (model: Subscription, options) => {
|
|
271
|
+
createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error);
|
|
272
|
+
},
|
|
273
|
+
afterUpdate: async (model: Subscription, options) => {
|
|
272
274
|
createEvent('Subscription', 'customer.subscription.updated', model, options).catch(console.error);
|
|
275
|
+
|
|
276
|
+
if (model.trail_start) {
|
|
277
|
+
const previousLatestInvoiceId = model.previous('latest_invoice_id');
|
|
278
|
+
|
|
279
|
+
if (!previousLatestInvoiceId && model.latest_invoice_id) {
|
|
280
|
+
createEvent('Subscription', 'customer.subscription.trial_start', model, options).catch(console.error);
|
|
281
|
+
} else if (
|
|
282
|
+
previousLatestInvoiceId &&
|
|
283
|
+
model.latest_invoice_id &&
|
|
284
|
+
previousLatestInvoiceId !== model.latest_invoice_id
|
|
285
|
+
) {
|
|
286
|
+
const count: number = await Invoice.count({
|
|
287
|
+
where: {
|
|
288
|
+
subscription_id: model.id,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
if (count === 2) {
|
|
292
|
+
// 当且仅当 有试用期的订阅更新了 && 恰好有 2 次发票,此时订阅的试用期刚好结束
|
|
293
|
+
createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else if (!model.previous('latest_invoice_id') && model.latest_invoice_id) {
|
|
297
|
+
createEvent('Subscription', 'customer.subscription.started', model, options).catch(console.error);
|
|
298
|
+
}
|
|
299
|
+
|
|
273
300
|
createStatusEvent('Subscription', 'customer.subscription', { canceled: 'deleted' }, model, options).catch(
|
|
274
301
|
console.error
|
|
275
302
|
);
|
|
@@ -313,15 +340,6 @@ export function getSubscriptionEventType(current: TSubscription, previous: Parti
|
|
|
313
340
|
if (!current.pause_collection && previous.pause_collection) {
|
|
314
341
|
return 'resumed';
|
|
315
342
|
}
|
|
316
|
-
if (
|
|
317
|
-
current.status === 'active' &&
|
|
318
|
-
current.current_period_end &&
|
|
319
|
-
current.current_period_end !== previous.current_period_end &&
|
|
320
|
-
current.current_period_start &&
|
|
321
|
-
current.current_period_start !== previous.current_period_start
|
|
322
|
-
) {
|
|
323
|
-
return 'renewed';
|
|
324
|
-
}
|
|
325
343
|
|
|
326
344
|
return '';
|
|
327
345
|
}
|
|
@@ -276,20 +276,17 @@ export type NftMintSettings = {
|
|
|
276
276
|
inputs?: Record<string, string>;
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
export interface NftMintItem {
|
|
280
|
+
tx_hash?: string;
|
|
281
|
+
address: string;
|
|
282
|
+
owner: string;
|
|
283
|
+
error?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
279
286
|
export type NftMintDetails = {
|
|
280
287
|
type: LiteralUnion<'arcblock' | 'ethereum' | 'bitcoin', string>;
|
|
281
|
-
arcblock?:
|
|
282
|
-
|
|
283
|
-
address: string;
|
|
284
|
-
owner: string;
|
|
285
|
-
error?: string;
|
|
286
|
-
};
|
|
287
|
-
ethereum?: {
|
|
288
|
-
tx_hash?: string;
|
|
289
|
-
address: string;
|
|
290
|
-
owner: string;
|
|
291
|
-
error?: string;
|
|
292
|
-
};
|
|
288
|
+
arcblock?: NftMintItem;
|
|
289
|
+
ethereum?: NftMintItem;
|
|
293
290
|
};
|
|
294
291
|
|
|
295
292
|
// Very similar to PaymentLink
|
|
@@ -406,7 +403,11 @@ export type EventType = LiteralUnion<
|
|
|
406
403
|
| 'customer.subscription.pending_update_expired'
|
|
407
404
|
| 'customer.subscription.resumed'
|
|
408
405
|
| 'customer.subscription.renewed'
|
|
406
|
+
| 'customer.subscription.renew_failed'
|
|
407
|
+
| 'customer.subscription.trial_start'
|
|
409
408
|
| 'customer.subscription.trial_will_end'
|
|
409
|
+
| 'customer.subscription.trial_end'
|
|
410
|
+
| 'customer.subscription.started'
|
|
410
411
|
| 'customer.subscription.updated'
|
|
411
412
|
| 'customer.tax_id.created'
|
|
412
413
|
| 'customer.tax_id.deleted'
|
package/api/third.d.ts
CHANGED
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.67",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@abtnode/cron": "^1.16.19",
|
|
43
44
|
"@arcblock/did": "^1.18.103",
|
|
44
45
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
45
46
|
"@arcblock/did-connect": "^2.8.20",
|
|
@@ -51,10 +52,10 @@
|
|
|
51
52
|
"@blocklet/ui-react": "^2.8.20",
|
|
52
53
|
"@blocklet/uploader": "^0.0.38",
|
|
53
54
|
"@mui/icons-material": "^5.14.19",
|
|
54
|
-
"@mui/lab": "^5.0.0-alpha.
|
|
55
|
-
"@mui/material": "^5.14.
|
|
56
|
-
"@mui/styles": "^5.14.
|
|
57
|
-
"@mui/system": "^5.14.
|
|
55
|
+
"@mui/lab": "^5.0.0-alpha.155",
|
|
56
|
+
"@mui/material": "^5.14.20",
|
|
57
|
+
"@mui/styles": "^5.14.20",
|
|
58
|
+
"@mui/system": "^5.14.20",
|
|
58
59
|
"@ocap/asset": "^1.18.103",
|
|
59
60
|
"@ocap/client": "^1.18.103",
|
|
60
61
|
"@ocap/mcrypto": "^1.18.103",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"lodash": "^4.17.21",
|
|
83
84
|
"morgan": "^1.10.0",
|
|
84
85
|
"nanoid": "3",
|
|
86
|
+
"p-all": "3.0.0",
|
|
85
87
|
"p-wait-for": "3",
|
|
86
88
|
"pretty-ms-i18n": "^1.0.3",
|
|
87
89
|
"react": "^18.2.0",
|
|
@@ -104,7 +106,7 @@
|
|
|
104
106
|
"@abtnode/types": "^1.16.19",
|
|
105
107
|
"@arcblock/eslint-config": "^0.2.4",
|
|
106
108
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
107
|
-
"@did-pay/types": "1.13.
|
|
109
|
+
"@did-pay/types": "1.13.67",
|
|
108
110
|
"@types/cookie-parser": "^1.4.6",
|
|
109
111
|
"@types/cors": "^2.8.17",
|
|
110
112
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -141,5 +143,5 @@
|
|
|
141
143
|
"parser": "typescript"
|
|
142
144
|
}
|
|
143
145
|
},
|
|
144
|
-
"gitHead": "
|
|
146
|
+
"gitHead": "37b1d2ce195a256f8743a6cc53fd24d3d01579b0"
|
|
145
147
|
}
|
package/src/app.tsx
CHANGED
|
@@ -19,6 +19,7 @@ const CheckoutPage = React.lazy(() => import('./pages/checkout'));
|
|
|
19
19
|
const AdminPage = React.lazy(() => import('./pages/admin'));
|
|
20
20
|
const CustomerHome = React.lazy(() => import('./pages/customer/index'));
|
|
21
21
|
const CustomerInvoice = React.lazy(() => import('./pages/customer/invoice'));
|
|
22
|
+
const CustomerSubscription = React.lazy(() => import('./pages/customer/subscription'));
|
|
22
23
|
|
|
23
24
|
const theme = createTheme({
|
|
24
25
|
typography: {
|
|
@@ -48,6 +49,7 @@ function App() {
|
|
|
48
49
|
<Route key="admin-sub" path="/admin/:group/:page" element={<AdminPage />} />,
|
|
49
50
|
<Route key="admin-fallback" path="/admin/*" element={<AdminPage />} />,
|
|
50
51
|
<Route key="customer-home" path="/customer" element={<CustomerHome />} />,
|
|
52
|
+
<Route key="customer-subscription" path="/customer/subscription/:id" element={<CustomerSubscription />} />
|
|
51
53
|
<Route key="customer-invoice" path="/customer/invoice/:id" element={<CustomerInvoice />} />,
|
|
52
54
|
<Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
|
|
53
55
|
<Route path="*" element={<Navigate to="/" />} />
|
|
@@ -15,13 +15,17 @@ type Props = {
|
|
|
15
15
|
data: TInvoiceExpanded;
|
|
16
16
|
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
17
17
|
onChange: (action: string) => void;
|
|
18
|
+
|
|
19
|
+
mode?: 'admin' | 'customer';
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
InvoiceActions.defaultProps = {
|
|
21
23
|
variant: 'compact',
|
|
24
|
+
|
|
25
|
+
mode: 'admin',
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
export default function InvoiceActions({ data, variant, onChange }: Props) {
|
|
28
|
+
export default function InvoiceActions({ data, variant, onChange, mode }: Props) {
|
|
25
29
|
const { t } = useLocaleContext();
|
|
26
30
|
const navigate = useNavigate();
|
|
27
31
|
const [state, setState] = useSetState({
|
|
@@ -43,6 +47,8 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
|
|
|
43
47
|
}
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
const isAdmin = mode === 'admin';
|
|
51
|
+
|
|
46
52
|
const actions = [
|
|
47
53
|
{
|
|
48
54
|
label: t('admin.invoice.download'),
|
|
@@ -50,13 +56,13 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
|
|
|
50
56
|
color: 'primary',
|
|
51
57
|
disabled: data.status !== 'paid',
|
|
52
58
|
},
|
|
53
|
-
{
|
|
59
|
+
isAdmin && {
|
|
54
60
|
label: t('admin.invoice.edit'),
|
|
55
61
|
handler: () => setState({ action: 'edit' }),
|
|
56
62
|
color: 'primary',
|
|
57
63
|
disabled: data.status !== 'draft',
|
|
58
64
|
},
|
|
59
|
-
{
|
|
65
|
+
isAdmin && {
|
|
60
66
|
label: t('admin.invoice.duplicate'),
|
|
61
67
|
handler: () => setState({ action: 'duplicate' }),
|
|
62
68
|
color: 'primary',
|
|
@@ -65,22 +71,34 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
|
|
|
65
71
|
},
|
|
66
72
|
{
|
|
67
73
|
label: t('admin.customer.view'),
|
|
68
|
-
handler: () =>
|
|
74
|
+
handler: () => {
|
|
75
|
+
if (isAdmin) {
|
|
76
|
+
navigate(`/admin/customers/${data.customer_id}`);
|
|
77
|
+
} else {
|
|
78
|
+
navigate('/customer');
|
|
79
|
+
}
|
|
80
|
+
},
|
|
69
81
|
color: 'primary',
|
|
70
82
|
},
|
|
71
|
-
];
|
|
83
|
+
].filter(Boolean);
|
|
72
84
|
|
|
73
85
|
if (variant === 'compact') {
|
|
74
86
|
actions.push({
|
|
75
87
|
label: t('admin.invoice.view'),
|
|
76
|
-
handler: () =>
|
|
88
|
+
handler: () => {
|
|
89
|
+
if (isAdmin) {
|
|
90
|
+
navigate(`/admin/billing/${data.id}`);
|
|
91
|
+
} else {
|
|
92
|
+
navigate(`/customer/invoice/${data.id}`);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
77
95
|
color: 'primary',
|
|
78
96
|
});
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
return (
|
|
82
100
|
<ClickBoundary>
|
|
83
|
-
<Actions variant={variant} actions={actions} />
|
|
101
|
+
<Actions variant={variant} actions={actions as any} />
|
|
84
102
|
{state.action === 'xxx' && (
|
|
85
103
|
<ConfirmDialog
|
|
86
104
|
onConfirm={handleAction}
|