enh
This commit is contained in:
parent
c3cfc10785
commit
e77982e5a6
13 changed files with 70 additions and 4 deletions
|
|
@ -89,6 +89,7 @@ export default function EditCoupon() {
|
||||||
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
|
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
|
||||||
validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined,
|
validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined,
|
||||||
maxLimitForUser: coupon.maxLimitForUser || undefined,
|
maxLimitForUser: coupon.maxLimitForUser || undefined,
|
||||||
|
isFirstOrderOnly: coupon.isFirstOrderOnly,
|
||||||
skuIds: coupon.skuIds,
|
skuIds: coupon.skuIds,
|
||||||
isReservedCoupon: false, // Normal coupons
|
isReservedCoupon: false, // Normal coupons
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ const couponValidationSchema = Yup.object().shape({
|
||||||
validTill: Yup.date().optional(),
|
validTill: Yup.date().optional(),
|
||||||
maxLimitForUser: Yup.number().min(1, 'Must be at least 1').optional(),
|
maxLimitForUser: Yup.number().min(1, 'Must be at least 1').optional(),
|
||||||
exclusiveApply: Yup.boolean().optional(),
|
exclusiveApply: Yup.boolean().optional(),
|
||||||
|
isFirstOrderOnly: Yup.boolean().optional(),
|
||||||
isUserBased: Yup.boolean(),
|
isUserBased: Yup.boolean(),
|
||||||
isApplyForAll: Yup.boolean(),
|
isApplyForAll: Yup.boolean(),
|
||||||
applicableUsers: Yup.array().of(Yup.number()).optional(),
|
applicableUsers: Yup.array().of(Yup.number()).optional(),
|
||||||
|
|
@ -133,6 +134,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
|
||||||
applicableUsers: [],
|
applicableUsers: [],
|
||||||
applicableProducts: [],
|
applicableProducts: [],
|
||||||
exclusiveApply: false,
|
exclusiveApply: false,
|
||||||
|
isFirstOrderOnly: false,
|
||||||
isReservedCoupon: false,
|
isReservedCoupon: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -379,6 +381,20 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* First Order Only */}
|
||||||
|
<View style={tw`mb-4`}>
|
||||||
|
<Text style={tw`text-base mb-2`}>First Order Only</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setFieldValue('isFirstOrderOnly', !values.isFirstOrderOnly)}
|
||||||
|
style={tw`flex-row items-center`}
|
||||||
|
>
|
||||||
|
<View style={tw`w-5 h-5 border-2 border-gray-300 rounded mr-3 ${values.isFirstOrderOnly ? 'bg-blue-500 border-blue-500' : ''}`}>
|
||||||
|
{values.isFirstOrderOnly && <Text style={tw`text-white text-center`}>✓</Text>}
|
||||||
|
</View>
|
||||||
|
<Text style={tw`text-gray-700`}>Valid only on the customer's first order</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Target Audience */}
|
{/* Target Audience */}
|
||||||
<Text style={tw`text-base font-bold mb-2 ${isReserved ? 'text-gray-400' : ''}`}>
|
<Text style={tw`text-base font-bold mb-2 ${isReserved ? 'text-gray-400' : ''}`}>
|
||||||
Target Audience {isReserved ? '(Disabled for Reserved Coupons)' : ''}
|
Target Audience {isReserved ? '(Disabled for Reserved Coupons)' : ''}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const createCouponBodySchema = z.object({
|
||||||
validTill: z.string().optional(),
|
validTill: z.string().optional(),
|
||||||
maxLimitForUser: z.number().optional(),
|
maxLimitForUser: z.number().optional(),
|
||||||
exclusiveApply: z.boolean().optional(),
|
exclusiveApply: z.boolean().optional(),
|
||||||
|
isFirstOrderOnly: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateCouponBodySchema = z.object({
|
const validateCouponBodySchema = z.object({
|
||||||
|
|
@ -50,7 +51,7 @@ export const couponRouter = router({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(createCouponBodySchema)
|
.input(createCouponBodySchema)
|
||||||
.mutation(async ({ input, ctx }): Promise<Coupon[]> => {
|
.mutation(async ({ input, ctx }): Promise<Coupon[]> => {
|
||||||
const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
|
const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply, isFirstOrderOnly } = input;
|
||||||
|
|
||||||
// Validation: ensure at least one discount type is provided
|
// Validation: ensure at least one discount type is provided
|
||||||
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
||||||
|
|
@ -114,6 +115,7 @@ export const couponRouter = router({
|
||||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
||||||
maxLimitForUser,
|
maxLimitForUser,
|
||||||
exclusiveApply: exclusiveApply || false,
|
exclusiveApply: exclusiveApply || false,
|
||||||
|
isFirstOrderOnly: isFirstOrderOnly || false,
|
||||||
},
|
},
|
||||||
applicableUsers,
|
applicableUsers,
|
||||||
applicableProducts
|
applicableProducts
|
||||||
|
|
@ -190,6 +192,7 @@ export const couponRouter = router({
|
||||||
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
|
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
|
||||||
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
|
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
|
||||||
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
|
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
|
||||||
|
if (updates.isFirstOrderOnly !== undefined) updateData.isFirstOrderOnly = updates.isFirstOrderOnly;
|
||||||
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
|
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
|
||||||
if (updates.skuIds !== undefined) updateData.skuIds = updates.skuIds;
|
if (updates.skuIds !== undefined) updateData.skuIds = updates.skuIds;
|
||||||
|
|
||||||
|
|
@ -375,7 +378,7 @@ export const couponRouter = router({
|
||||||
createReservedCoupon: protectedProcedure
|
createReservedCoupon: protectedProcedure
|
||||||
.input(createCouponBodySchema)
|
.input(createCouponBodySchema)
|
||||||
.mutation(async ({ input, ctx }): Promise<any> => {
|
.mutation(async ({ input, ctx }): Promise<any> => {
|
||||||
const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
|
const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply, isFirstOrderOnly } = input;
|
||||||
|
|
||||||
// Validation: ensure at least one discount type is provided
|
// Validation: ensure at least one discount type is provided
|
||||||
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
|
||||||
|
|
@ -419,6 +422,7 @@ export const couponRouter = router({
|
||||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
||||||
maxLimitForUser,
|
maxLimitForUser,
|
||||||
exclusiveApply: exclusiveApply || false,
|
exclusiveApply: exclusiveApply || false,
|
||||||
|
isFirstOrderOnly: isFirstOrderOnly || false,
|
||||||
createdBy: staffUserId,
|
createdBy: staffUserId,
|
||||||
},
|
},
|
||||||
applicableProducts
|
applicableProducts
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getUserAllCouponsWithRelations as getUserAllCouponsWithRelationsInDb,
|
getUserAllCouponsWithRelations as getUserAllCouponsWithRelationsInDb,
|
||||||
getUserReservedCouponByCode as getUserReservedCouponByCodeInDb,
|
getUserReservedCouponByCode as getUserReservedCouponByCodeInDb,
|
||||||
redeemUserReservedCoupon as redeemUserReservedCouponInDb,
|
redeemUserReservedCoupon as redeemUserReservedCouponInDb,
|
||||||
|
getUserOrderCount as getUserOrderCountInDb,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
UserCouponDisplay,
|
UserCouponDisplay,
|
||||||
|
|
@ -72,7 +73,9 @@ export const userCouponRouter = router({
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Filter to only coupons applicable to current user
|
// Filter to only coupons applicable to current user
|
||||||
|
const isFirstOrder = (await getUserOrderCountInDb(userId)) === 0;
|
||||||
const applicableCoupons = allCoupons.filter(coupon => {
|
const applicableCoupons = allCoupons.filter(coupon => {
|
||||||
|
if (coupon.isFirstOrderOnly && !isFirstOrder) return false;
|
||||||
if(!coupon.isUserBased) return true;
|
if(!coupon.isUserBased) return true;
|
||||||
const applicableUsers = coupon.applicableUsers || [];
|
const applicableUsers = coupon.applicableUsers || [];
|
||||||
return applicableUsers.some(au => au.userId === userId);
|
return applicableUsers.some(au => au.userId === userId);
|
||||||
|
|
@ -108,13 +111,15 @@ export const userCouponRouter = router({
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Filter coupons in JS: not invalidated, applicable to user, and not expired
|
// Filter coupons in JS: not invalidated, applicable to user, not expired, and first-order eligible
|
||||||
|
const isFirstOrder = (await getUserOrderCountInDb(userId)) === 0;
|
||||||
const applicableCoupons = allCoupons.filter(coupon => {
|
const applicableCoupons = allCoupons.filter(coupon => {
|
||||||
const isNotInvalidated = !coupon.isInvalidated;
|
const isNotInvalidated = !coupon.isInvalidated;
|
||||||
const applicableUsers = coupon.applicableUsers || [];
|
const applicableUsers = coupon.applicableUsers || [];
|
||||||
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
|
const isApplicable = coupon.isApplyForAll || applicableUsers.some(au => au.userId === userId);
|
||||||
const isNotExpired = !coupon.validTill || new Date(coupon.validTill) > new Date();
|
const isNotExpired = !coupon.validTill || new Date(coupon.validTill) > new Date();
|
||||||
return isNotInvalidated && isApplicable && isNotExpired;
|
const isFirstOrderEligible = !coupon.isFirstOrderOnly || isFirstOrder;
|
||||||
|
return isNotInvalidated && isApplicable && isNotExpired && isFirstOrderEligible;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Categorize coupons
|
// Categorize coupons
|
||||||
|
|
@ -186,6 +191,11 @@ export const userCouponRouter = router({
|
||||||
throw new ApiError("You have already redeemed this coupon", 400);
|
throw new ApiError("You have already redeemed this coupon", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFirstOrder = (await getUserOrderCountInDb(userId)) === 0;
|
||||||
|
if (reservedCoupon.isFirstOrderOnly && !isFirstOrder) {
|
||||||
|
throw new ApiError("This is first order only coupon. You already placed your first order. This coupon doesn't apply to you", 400);
|
||||||
|
}
|
||||||
|
|
||||||
const couponResult = await redeemUserReservedCouponInDb(userId, reservedCoupon)
|
const couponResult = await redeemUserReservedCouponInDb(userId, reservedCoupon)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Migration: Add is_first_order_only flag to coupons and reserved_coupons
|
||||||
|
-- When true, the coupon can only be applied to a user's first order.
|
||||||
|
|
||||||
|
ALTER TABLE `coupons` ADD COLUMN `is_first_order_only` integer DEFAULT false NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `reserved_coupons` ADD COLUMN `is_first_order_only` integer DEFAULT false NOT NULL;
|
||||||
|
|
@ -22,6 +22,13 @@
|
||||||
"when": 1785336600165,
|
"when": 1785336600165,
|
||||||
"tag": "0002_sku_split",
|
"tag": "0002_sku_split",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1789236103378,
|
||||||
|
"tag": "0003_first_order_only",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ export interface Coupon {
|
||||||
validTill: Date | null
|
validTill: Date | null
|
||||||
maxLimitForUser: number | null
|
maxLimitForUser: number | null
|
||||||
exclusiveApply: boolean
|
exclusiveApply: boolean
|
||||||
|
isFirstOrderOnly: boolean
|
||||||
isInvalidated: boolean
|
isInvalidated: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
createdBy: number
|
createdBy: number
|
||||||
|
|
@ -100,6 +101,7 @@ export interface CreateCouponInput {
|
||||||
validTill?: Date
|
validTill?: Date
|
||||||
maxLimitForUser?: number
|
maxLimitForUser?: number
|
||||||
exclusiveApply: boolean
|
exclusiveApply: boolean
|
||||||
|
isFirstOrderOnly?: boolean
|
||||||
createdBy: number
|
createdBy: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +124,7 @@ export async function createCouponWithRelations(
|
||||||
validTill: input.validTill,
|
validTill: input.validTill,
|
||||||
maxLimitForUser: input.maxLimitForUser,
|
maxLimitForUser: input.maxLimitForUser,
|
||||||
exclusiveApply: input.exclusiveApply,
|
exclusiveApply: input.exclusiveApply,
|
||||||
|
isFirstOrderOnly: input.isFirstOrderOnly ?? false,
|
||||||
}).returning()
|
}).returning()
|
||||||
|
|
||||||
if (applicableUsers && applicableUsers.length > 0) {
|
if (applicableUsers && applicableUsers.length > 0) {
|
||||||
|
|
@ -158,6 +161,7 @@ export interface UpdateCouponInput {
|
||||||
validTill?: Date | null
|
validTill?: Date | null
|
||||||
maxLimitForUser?: number
|
maxLimitForUser?: number
|
||||||
exclusiveApply?: boolean
|
exclusiveApply?: boolean
|
||||||
|
isFirstOrderOnly?: boolean
|
||||||
isInvalidated?: boolean
|
isInvalidated?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,6 +332,7 @@ export async function createReservedCouponWithProducts(
|
||||||
validTill: input.validTill,
|
validTill: input.validTill,
|
||||||
maxLimitForUser: input.maxLimitForUser,
|
maxLimitForUser: input.maxLimitForUser,
|
||||||
exclusiveApply: input.exclusiveApply,
|
exclusiveApply: input.exclusiveApply,
|
||||||
|
isFirstOrderOnly: input.isFirstOrderOnly ?? false,
|
||||||
createdBy: input.createdBy,
|
createdBy: input.createdBy,
|
||||||
}).returning()
|
}).returning()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,7 @@ export const coupons = sqliteTable('coupons', {
|
||||||
maxLimitForUser: integer('max_limit_for_user'),
|
maxLimitForUser: integer('max_limit_for_user'),
|
||||||
isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false),
|
isInvalidated: integer('is_invalidated', { mode: 'boolean' }).notNull().default(false),
|
||||||
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
|
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
isFirstOrderOnly: integer('is_first_order_only', { mode: 'boolean' }).notNull().default(false),
|
||||||
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -520,6 +521,7 @@ export const reservedCoupons = sqliteTable('reserved_coupons', {
|
||||||
validTill: timestampText('valid_till'),
|
validTill: timestampText('valid_till'),
|
||||||
maxLimitForUser: integer('max_limit_for_user'),
|
maxLimitForUser: integer('max_limit_for_user'),
|
||||||
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
|
exclusiveApply: integer('exclusive_apply', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
isFirstOrderOnly: integer('is_first_order_only', { mode: 'boolean' }).notNull().default(false),
|
||||||
isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false),
|
isRedeemed: integer('is_redeemed', { mode: 'boolean' }).notNull().default(false),
|
||||||
redeemedBy: integer('redeemed_by').references(() => users.id),
|
redeemedBy: integer('redeemed_by').references(() => users.id),
|
||||||
redeemedAt: timestampText('redeemed_at'),
|
redeemedAt: timestampText('redeemed_at'),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const mapCoupon = (coupon: CouponRow): UserCoupon => ({
|
||||||
maxLimitForUser: coupon.maxLimitForUser ?? null,
|
maxLimitForUser: coupon.maxLimitForUser ?? null,
|
||||||
isInvalidated: coupon.isInvalidated,
|
isInvalidated: coupon.isInvalidated,
|
||||||
exclusiveApply: coupon.exclusiveApply,
|
exclusiveApply: coupon.exclusiveApply,
|
||||||
|
isFirstOrderOnly: coupon.isFirstOrderOnly,
|
||||||
createdAt: coupon.createdAt,
|
createdAt: coupon.createdAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ export async function redeemReservedCoupon(userId: number, reservedCoupon: Reser
|
||||||
validTill: reservedCoupon.validTill,
|
validTill: reservedCoupon.validTill,
|
||||||
maxLimitForUser: reservedCoupon.maxLimitForUser,
|
maxLimitForUser: reservedCoupon.maxLimitForUser,
|
||||||
exclusiveApply: reservedCoupon.exclusiveApply,
|
exclusiveApply: reservedCoupon.exclusiveApply,
|
||||||
|
isFirstOrderOnly: reservedCoupon.isFirstOrderOnly,
|
||||||
createdBy: reservedCoupon.createdBy,
|
createdBy: reservedCoupon.createdBy,
|
||||||
}).returning()
|
}).returning()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,15 @@ export async function validateAndGetCoupon(
|
||||||
)
|
)
|
||||||
throw new Error('Order amount does not meet coupon minimum requirement')
|
throw new Error('Order amount does not meet coupon minimum requirement')
|
||||||
|
|
||||||
|
if (coupon.isFirstOrderOnly) {
|
||||||
|
const orderCount = await getOrderCount(userId)
|
||||||
|
if (orderCount > 0) {
|
||||||
|
throw new Error(
|
||||||
|
"This is first order only coupon. You already placed your first order. This coupon doesn't apply to you"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return coupon as CouponValidationResult
|
return coupon as CouponValidationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export interface Coupon {
|
||||||
validTill: Date | null;
|
validTill: Date | null;
|
||||||
maxLimitForUser: number | null;
|
maxLimitForUser: number | null;
|
||||||
exclusiveApply: boolean;
|
exclusiveApply: boolean;
|
||||||
|
isFirstOrderOnly: boolean;
|
||||||
isInvalidated: boolean;
|
isInvalidated: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,7 @@ export interface UserCoupon {
|
||||||
maxLimitForUser: number | null;
|
maxLimitForUser: number | null;
|
||||||
isInvalidated: boolean;
|
isInvalidated: boolean;
|
||||||
exclusiveApply: boolean;
|
exclusiveApply: boolean;
|
||||||
|
isFirstOrderOnly?: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ export interface Coupon {
|
||||||
isApplyForAll: boolean;
|
isApplyForAll: boolean;
|
||||||
validTill: Date | null;
|
validTill: Date | null;
|
||||||
maxLimitForUser: number | null;
|
maxLimitForUser: number | null;
|
||||||
|
isFirstOrderOnly: boolean;
|
||||||
isInvalidated: boolean;
|
isInvalidated: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
@ -285,6 +286,7 @@ export interface CreateCouponPayload {
|
||||||
applicableUsers?: number[];
|
applicableUsers?: number[];
|
||||||
applicableProducts?: number[];
|
applicableProducts?: number[];
|
||||||
exclusiveApply?: boolean;
|
exclusiveApply?: boolean;
|
||||||
|
isFirstOrderOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patient History Types
|
// Patient History Types
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue