@goweekdays/layer-common 0.0.6 → 0.0.7
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/CHANGELOG.md +6 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +39 -17
- package/composables/useOrder.ts +22 -0
- package/composables/usePromoCode.ts +34 -0
- package/composables/useSubscription.ts +40 -23
- package/composables/useUtils.ts +27 -0
- package/package.json +1 -1
- package/pages/index.vue +3 -0
- package/pages/payment-method-linked.vue +31 -0
- package/public/paymaya-logo.png +0 -0
- package/types/promo-code.d.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defineModel, defineProps, computed, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
rules?: ((value: string) => boolean | string)[]; // Accepts external validation rules
|
|
6
|
+
}>();
|
|
7
|
+
|
|
8
|
+
const dateValue = defineModel<string>({ default: "" });
|
|
9
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
10
|
+
|
|
11
|
+
const formatDate = (event: Event) => {
|
|
12
|
+
const input = event.target as HTMLInputElement;
|
|
13
|
+
let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
|
|
14
|
+
|
|
15
|
+
// Format as MM/DD/YYYY
|
|
16
|
+
let formattedValue = value
|
|
17
|
+
.slice(0, 8)
|
|
18
|
+
.replace(/(\d{2})(\d{0,2})?(\d{0,4})?/, (_, m, d, y) =>
|
|
19
|
+
[m, d, y].filter(Boolean).join("/")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Preserve cursor position
|
|
23
|
+
const cursorPosition = input.selectionStart ?? 0;
|
|
24
|
+
const slashCountBefore = (dateValue.value.match(/\//g) || []).length;
|
|
25
|
+
const slashCountAfter = (formattedValue.match(/\//g) || []).length;
|
|
26
|
+
const cursorOffset = slashCountAfter - slashCountBefore;
|
|
27
|
+
|
|
28
|
+
// Only update if value changed to prevent unnecessary reactivity updates
|
|
29
|
+
if (dateValue.value !== formattedValue) {
|
|
30
|
+
dateValue.value = formattedValue;
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
input.setSelectionRange(
|
|
33
|
+
cursorPosition + cursorOffset,
|
|
34
|
+
cursorPosition + cursorOffset
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Compute combined validation rules
|
|
41
|
+
const computedRules = computed(() => {
|
|
42
|
+
return props.rules ? [...props.rules] : [];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Handle arrow key increments with cursor preservation
|
|
46
|
+
const handleArrowKeys = (event: KeyboardEvent) => {
|
|
47
|
+
if (!dateValue.value) return;
|
|
48
|
+
|
|
49
|
+
const input = event.target as HTMLInputElement;
|
|
50
|
+
const cursorPosition = input.selectionStart ?? 0; // Store cursor position
|
|
51
|
+
dateValue.value.split("/").map(Number);
|
|
52
|
+
|
|
53
|
+
let updatedDate = dateValue.value;
|
|
54
|
+
|
|
55
|
+
// Determine which part to modify
|
|
56
|
+
if (cursorPosition <= 2) {
|
|
57
|
+
updatedDate = modifyDatePart(
|
|
58
|
+
dateValue.value,
|
|
59
|
+
"month",
|
|
60
|
+
event.key === "ArrowUp" ? 1 : -1
|
|
61
|
+
);
|
|
62
|
+
} else if (cursorPosition <= 5) {
|
|
63
|
+
updatedDate = modifyDatePart(
|
|
64
|
+
dateValue.value,
|
|
65
|
+
"day",
|
|
66
|
+
event.key === "ArrowUp" ? 1 : -1
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
updatedDate = modifyDatePart(
|
|
70
|
+
dateValue.value,
|
|
71
|
+
"year",
|
|
72
|
+
event.key === "ArrowUp" ? 1 : -1
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (dateValue.value !== updatedDate) {
|
|
77
|
+
dateValue.value = updatedDate;
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
input.setSelectionRange(cursorPosition, cursorPosition);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const modifyDatePart = (
|
|
87
|
+
date: string,
|
|
88
|
+
part: "month" | "day" | "year",
|
|
89
|
+
change: number
|
|
90
|
+
) => {
|
|
91
|
+
let [month, day, year] = date.split("/").map(Number);
|
|
92
|
+
|
|
93
|
+
if (part === "month") {
|
|
94
|
+
month = Math.max(1, Math.min(12, month + change));
|
|
95
|
+
const maxDays = new Date(year, month, 0).getDate();
|
|
96
|
+
day = Math.min(day, maxDays); // Adjust day to fit new month's max days
|
|
97
|
+
} else if (part === "day") {
|
|
98
|
+
const maxDays = new Date(year, month, 0).getDate();
|
|
99
|
+
day = Math.max(1, Math.min(maxDays, day + change));
|
|
100
|
+
} else if (part === "year") {
|
|
101
|
+
year += change;
|
|
102
|
+
const maxDays = new Date(year, month, 0).getDate();
|
|
103
|
+
day = Math.min(day, maxDays); // Adjust day to fit new year's month
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `${String(month).padStart(2, "0")}/${String(day).padStart(
|
|
107
|
+
2,
|
|
108
|
+
"0"
|
|
109
|
+
)}/${year}`;
|
|
110
|
+
};
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<v-text-field
|
|
115
|
+
ref="inputRef"
|
|
116
|
+
v-model="dateValue"
|
|
117
|
+
placeholder="MM/DD/YYYY"
|
|
118
|
+
@input="formatDate"
|
|
119
|
+
@keydown.up="handleArrowKeys"
|
|
120
|
+
@keydown.down="handleArrowKeys"
|
|
121
|
+
:rules="computedRules"
|
|
122
|
+
></v-text-field>
|
|
123
|
+
</template>
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, nextTick, useAttrs } from "vue";
|
|
3
3
|
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
infinityEnabled: { type: Boolean, default: false }, // Enable ∞ display for 0
|
|
6
|
+
});
|
|
7
|
+
|
|
4
8
|
const modelValue = defineModel<number>(); // Store actual number
|
|
5
|
-
const inputRef = ref<HTMLInputElement | null>(null);
|
|
6
|
-
const attrs = useAttrs();
|
|
9
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
10
|
+
const attrs = useAttrs();
|
|
7
11
|
|
|
8
|
-
let cursorPosition = 0;
|
|
9
|
-
let forceCursorToEnd = false;
|
|
12
|
+
let cursorPosition = 0;
|
|
13
|
+
let forceCursorToEnd = false;
|
|
10
14
|
|
|
11
|
-
// Computed property to format value with commas
|
|
15
|
+
// Computed property to format value with commas or infinity sign
|
|
12
16
|
const formattedValue = computed({
|
|
13
|
-
get: () =>
|
|
14
|
-
|
|
17
|
+
get: () => {
|
|
18
|
+
if (props.infinityEnabled && modelValue.value === 0) return "∞"; // Show ∞ if enabled
|
|
19
|
+
return modelValue.value?.toLocaleString() ?? "";
|
|
20
|
+
},
|
|
15
21
|
set: (val: string) => {
|
|
22
|
+
if (props.infinityEnabled && val === "∞") {
|
|
23
|
+
modelValue.value = 0; // Convert back to 0 internally
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
const rawValue = val.replace(/\D/g, ""); // Remove non-numeric characters
|
|
17
28
|
const numericValue = rawValue ? Number(rawValue) : 0;
|
|
18
29
|
|
|
@@ -30,7 +41,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
30
41
|
const { selectionStart, selectionEnd, value } = inputRef.value;
|
|
31
42
|
const isAllSelected = selectionStart === 0 && selectionEnd === value.length;
|
|
32
43
|
|
|
33
|
-
// Allow only numbers (prevent non-numeric input)
|
|
34
44
|
if (
|
|
35
45
|
!/^\d$/.test(event.key) &&
|
|
36
46
|
![
|
|
@@ -46,7 +56,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
46
56
|
return;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
// If all text is selected and a number is pressed, replace entire value
|
|
50
59
|
if (isAllSelected && /^\d$/.test(event.key)) {
|
|
51
60
|
event.preventDefault();
|
|
52
61
|
modelValue.value = Number(event.key);
|
|
@@ -55,13 +64,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
// Allow cursor movement with ArrowLeft & ArrowRight
|
|
59
67
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
60
68
|
forceCursorToEnd = false;
|
|
61
69
|
return;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
// Force cursor to end when incrementing/decrementing
|
|
65
72
|
if (event.key === "ArrowUp") {
|
|
66
73
|
event.preventDefault();
|
|
67
74
|
modelValue.value += 1;
|
|
@@ -71,21 +78,21 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
71
78
|
modelValue.value = Math.max(0, modelValue.value - 1);
|
|
72
79
|
forceCursorToEnd = true;
|
|
73
80
|
} else {
|
|
74
|
-
cursorPosition = selectionStart || 0;
|
|
81
|
+
cursorPosition = selectionStart || 0;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
nextTick(() => restoreCursor());
|
|
78
85
|
};
|
|
79
86
|
|
|
80
|
-
// Restore cursor position
|
|
87
|
+
// Restore cursor position
|
|
81
88
|
const restoreCursor = () => {
|
|
82
89
|
if (!inputRef.value) return;
|
|
83
90
|
const length = formattedValue.value.length;
|
|
84
91
|
|
|
85
92
|
if (forceCursorToEnd) {
|
|
86
|
-
inputRef.value.setSelectionRange(length, length);
|
|
93
|
+
inputRef.value.setSelectionRange(length, length);
|
|
87
94
|
} else {
|
|
88
|
-
inputRef.value.setSelectionRange(cursorPosition, cursorPosition);
|
|
95
|
+
inputRef.value.setSelectionRange(cursorPosition, cursorPosition);
|
|
89
96
|
}
|
|
90
97
|
};
|
|
91
98
|
</script>
|
|
@@ -96,7 +103,22 @@ const restoreCursor = () => {
|
|
|
96
103
|
v-model="formattedValue"
|
|
97
104
|
v-bind="attrs"
|
|
98
105
|
type="text"
|
|
99
|
-
placeholder="Enter a number"
|
|
100
106
|
@keydown="handleKeyDown"
|
|
101
|
-
|
|
107
|
+
>
|
|
108
|
+
<template v-if="$slots.prepend" v-slot:prepend>
|
|
109
|
+
<slot name="prepend"></slot>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<template v-if="$slots.append" v-slot:append>
|
|
113
|
+
<slot name="append"></slot>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template v-if="$slots['append-inner']" v-slot:append-inner>
|
|
117
|
+
<slot name="append-inner"></slot>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template v-if="$slots['prepend-inner']" v-slot:prepend-inner>
|
|
121
|
+
<slot name="prepend-inner"></slot>
|
|
122
|
+
</template>
|
|
123
|
+
</v-text-field>
|
|
102
124
|
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default function useOrder() {
|
|
2
|
+
function getOrders({
|
|
3
|
+
search = "",
|
|
4
|
+
id = "",
|
|
5
|
+
page = 1,
|
|
6
|
+
status = "succeeded",
|
|
7
|
+
} = {}) {
|
|
8
|
+
return useNuxtApp().$api<Record<string, any>>("/api/orders", {
|
|
9
|
+
method: "GET",
|
|
10
|
+
query: {
|
|
11
|
+
search,
|
|
12
|
+
id,
|
|
13
|
+
page,
|
|
14
|
+
status,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
getOrders,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default function usePromoCode() {
|
|
2
|
+
function add(value: TPromoCode) {
|
|
3
|
+
return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
body: value,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getPromoCodes({ search = "", page = 1, status = "active" } = {}) {
|
|
10
|
+
return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
|
|
11
|
+
method: "GET",
|
|
12
|
+
query: {
|
|
13
|
+
search,
|
|
14
|
+
page,
|
|
15
|
+
status,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getByCode(code: string) {
|
|
21
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
22
|
+
`/api/promo-codes/code/${code}`,
|
|
23
|
+
{
|
|
24
|
+
method: "GET",
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
add,
|
|
31
|
+
getPromoCodes,
|
|
32
|
+
getByCode,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -13,6 +13,12 @@ export default function useSubscription() {
|
|
|
13
13
|
return useNuxtApp().$api<TSubscription>(`/api/subscriptions/id/${id}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function getByAffiliateId(id: string) {
|
|
17
|
+
return useNuxtApp().$api<TSubscription>(
|
|
18
|
+
`/api/subscriptions/affiliate/${id}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
function getByOrgId(id: string) {
|
|
17
23
|
return useNuxtApp().$api<Record<string, any>>(
|
|
18
24
|
`/api/subscriptions/org/${id}`
|
|
@@ -40,37 +46,35 @@ export default function useSubscription() {
|
|
|
40
46
|
() => "inactive"
|
|
41
47
|
);
|
|
42
48
|
|
|
43
|
-
async function
|
|
49
|
+
async function affSubscriptionStatus() {
|
|
44
50
|
const { currentUser } = useLocalAuth();
|
|
45
51
|
|
|
46
52
|
if (currentUser.value && currentUser.value._id) {
|
|
47
53
|
try {
|
|
48
|
-
const result = await
|
|
49
|
-
affiliateSubscription.value = result.status;
|
|
54
|
+
const result = await getByAffiliateId(currentUser.value._id);
|
|
55
|
+
affiliateSubscription.value = result.status as string;
|
|
50
56
|
} catch (error) {
|
|
51
57
|
console.error("failed to get the subscription", error);
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
});
|
|
62
|
+
const orgSubscription = useState("orgSubscription", () => "inactive");
|
|
63
|
+
|
|
64
|
+
async function orgSubscriptionStatus() {
|
|
65
|
+
const { currentOrg } = useOrg();
|
|
66
|
+
|
|
67
|
+
if (currentOrg.value) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await getByOrgId(currentOrg.value);
|
|
70
|
+
orgSubscription.value = result.status as string;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("failed to get the subscription", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
type
|
|
77
|
+
type TSub = {
|
|
74
78
|
customer_id: string;
|
|
75
79
|
amount: number;
|
|
76
80
|
payment_method_id: string;
|
|
@@ -82,12 +86,12 @@ export default function useSubscription() {
|
|
|
82
86
|
payment_method_cvv?: string;
|
|
83
87
|
payment_method_cardholder_name?: string;
|
|
84
88
|
currency?: string;
|
|
85
|
-
seats
|
|
86
|
-
organization
|
|
89
|
+
seats?: number;
|
|
90
|
+
organization?: TOrg;
|
|
87
91
|
billingAddress: TAddress;
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
function initOrgSubscription(value:
|
|
94
|
+
function initOrgSubscription(value: TSub) {
|
|
91
95
|
return useNuxtApp().$api<Record<string, any>>(
|
|
92
96
|
"/api/subscriptions/organization",
|
|
93
97
|
{
|
|
@@ -97,16 +101,29 @@ export default function useSubscription() {
|
|
|
97
101
|
);
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
function initAffiliateSubscription(value: TSub) {
|
|
105
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
106
|
+
"/api/subscriptions/affiliate",
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: value,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
return {
|
|
101
115
|
add,
|
|
102
116
|
getById,
|
|
103
117
|
getByOrgId,
|
|
104
118
|
getSubscriptions,
|
|
119
|
+
affSubscriptionStatus,
|
|
105
120
|
affiliateSubscription,
|
|
106
|
-
|
|
121
|
+
orgSubscriptionStatus,
|
|
122
|
+
orgSubscription,
|
|
107
123
|
getSubscriptionStatusById,
|
|
108
124
|
cancelSubscription,
|
|
109
125
|
initAffiliateSubscription,
|
|
110
126
|
initOrgSubscription,
|
|
127
|
+
getByAffiliateId,
|
|
111
128
|
};
|
|
112
129
|
}
|
package/composables/useUtils.ts
CHANGED
|
@@ -6,6 +6,31 @@ export default function useUtils() {
|
|
|
6
6
|
return regex.test(v) || "Please enter a valid email address";
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
const validateDate = (value: string): boolean | string => {
|
|
10
|
+
const dateRegex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
|
|
11
|
+
|
|
12
|
+
if (!dateRegex.test(value)) return "Invalid date format (MM/DD/YYYY)";
|
|
13
|
+
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const minOneMonthAdvance = (value: string): boolean | string => {
|
|
18
|
+
if (!/^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/.test(value)) {
|
|
19
|
+
return "Invalid date format (MM/DD/YYYY)";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [month, day, year] = value.split("/").map(Number);
|
|
23
|
+
const selectedDate = new Date(year, month - 1, day);
|
|
24
|
+
|
|
25
|
+
const currentDate = new Date();
|
|
26
|
+
const minDate = new Date();
|
|
27
|
+
minDate.setMonth(currentDate.getMonth() + 1); // 1 month in advance
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
selectedDate >= minDate || "Date must be at least 1 month in advance"
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
9
34
|
const passwordRule = (v: string) =>
|
|
10
35
|
v.length >= 8 || "Password must be at least 8 characters long";
|
|
11
36
|
|
|
@@ -180,5 +205,7 @@ export default function useUtils() {
|
|
|
180
205
|
getCountries,
|
|
181
206
|
formatter,
|
|
182
207
|
formatNumber,
|
|
208
|
+
validateDate,
|
|
209
|
+
minOneMonthAdvance,
|
|
183
210
|
};
|
|
184
211
|
}
|
package/package.json
CHANGED
package/pages/index.vue
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="fill-height" justify="center" align-content="center">
|
|
3
|
+
<v-card class="pa-4" elevation="4" color="success lighten-1">
|
|
4
|
+
<v-card-title class="text-white d-flex align-center">
|
|
5
|
+
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
|
6
|
+
Successfully Linked Payment Method
|
|
7
|
+
</v-card-title>
|
|
8
|
+
|
|
9
|
+
<v-card-actions class="d-flex justify-center">
|
|
10
|
+
<v-btn
|
|
11
|
+
color="white"
|
|
12
|
+
variant="tonal"
|
|
13
|
+
rounded="xl"
|
|
14
|
+
size="large"
|
|
15
|
+
@click="closeWindow"
|
|
16
|
+
>Close</v-btn
|
|
17
|
+
>
|
|
18
|
+
</v-card-actions>
|
|
19
|
+
</v-card>
|
|
20
|
+
</v-row>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
definePageMeta({
|
|
25
|
+
layout: "plain",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const closeWindow = () => {
|
|
29
|
+
window.close();
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare type TPromoTier = {
|
|
2
|
+
min: number;
|
|
3
|
+
max: number;
|
|
4
|
+
price: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare type TPromoCode = {
|
|
8
|
+
_id?: ObjectId;
|
|
9
|
+
code: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
type: "tiered" | "fixed";
|
|
12
|
+
tiers?: TPromoTier[];
|
|
13
|
+
fixed_rate?: number;
|
|
14
|
+
appliesTo?: string; // "subscription" | "order"
|
|
15
|
+
createdAt?: string; // Date of creation
|
|
16
|
+
expiresAt?: string; // Optional expiry
|
|
17
|
+
assignedTo?: string | ObjectId; // Store the only ape who used it
|
|
18
|
+
status?: "active" | "expired" | "disabled";
|
|
19
|
+
};
|