fix(checkout,orders,admin): resolve guest checkout address bug, persist refill and charity in DB, and fix admin media thumbnail cropping
This commit is contained in:
parent
989f77e106
commit
4979d4120c
@ -7,12 +7,12 @@
|
||||
"status": "COMPLETE",
|
||||
"checkpoint": {
|
||||
"active_agent": "01_auditor",
|
||||
"current_ticket_id": "EPIC-05-TASK-01",
|
||||
"current_ticket_id": "EPIC-06-TASK-05",
|
||||
"sub_step": {
|
||||
"index": 3,
|
||||
"total": 3,
|
||||
"name": "verify_all_builds",
|
||||
"description": "All 11 epic tasks fully completed and verified with production builds"
|
||||
"index": 5,
|
||||
"total": 5,
|
||||
"name": "epic_06_verification",
|
||||
"description": "All EPIC-06 tasks (Guest Checkout, Auto-Refill, Charity Round-Up, Dynamic AI Depletion & Admin Media Gallery) fully resolved and verified"
|
||||
}
|
||||
},
|
||||
"review_phase": {
|
||||
|
||||
@ -288,6 +288,8 @@ model Order {
|
||||
couponId String? @map("coupon_id") @db.Uuid
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(15, 2)
|
||||
charityDonation Decimal @default(0.00) @map("charity_donation") @db.Decimal(15, 2)
|
||||
isRefill Boolean @default(false) @map("is_refill")
|
||||
refillIntervalDays Int? @map("refill_interval_days")
|
||||
status String @default("processing") @db.VarChar(30) // processing, shipped, delivered
|
||||
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
@ -30,6 +30,25 @@ export class CreateOrderDto {
|
||||
@IsString()
|
||||
prescriptionUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'مبلغ کمک به پناهگاه حیوانات (ردپای مهربانی)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
charityDonation?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'فعالسازی تمدید خودکار سفارش' })
|
||||
@IsOptional()
|
||||
isRefill?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'دوره زمانی تمدید خودکار (روز)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
refillIntervalDays?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'آدرس تحویل گیرنده' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
shippingAddress?: string;
|
||||
|
||||
@ApiProperty({ description: 'لیست اقلام سفارش', type: [OrderItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
@ -106,14 +106,36 @@ export class OrdersService {
|
||||
}
|
||||
}
|
||||
|
||||
const finalAmount = Math.max(totalAmount - discountAmount, 0);
|
||||
// Apply Refill 5% discount if requested
|
||||
let refillDiscount = 0;
|
||||
if (createOrderDto.isRefill) {
|
||||
refillDiscount = totalAmount * 0.05;
|
||||
}
|
||||
|
||||
const charityAmount = Number(createOrderDto.charityDonation || 0);
|
||||
const finalAmount = Math.max(totalAmount - discountAmount - refillDiscount + charityAmount, 0);
|
||||
const trackingNumber = this.generateTrackingNumber();
|
||||
|
||||
// Increment user charity total if charity donation was added
|
||||
if (charityAmount > 0) {
|
||||
try {
|
||||
await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { charityDonationTotal: { increment: charityAmount } }
|
||||
});
|
||||
} catch {
|
||||
// Ignore if user not found or guest
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.order.create({
|
||||
data: {
|
||||
userId,
|
||||
couponId,
|
||||
totalAmount: finalAmount,
|
||||
charityDonation: charityAmount,
|
||||
isRefill: Boolean(createOrderDto.isRefill),
|
||||
refillIntervalDays: createOrderDto.refillIntervalDays || 60,
|
||||
trackingNumber,
|
||||
status: 'processing',
|
||||
orderItems: {
|
||||
|
||||
@ -197,11 +197,11 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
|
||||
}}
|
||||
className={`group relative bg-gray-100 rounded-xl overflow-hidden border-2 cursor-pointer transition-all hover:shadow-lg hover:shadow-purple-100 ${isSelected ? 'border-purple-600 ring-2 ring-purple-300' : 'border-transparent hover:border-purple-500'}`}
|
||||
>
|
||||
<div className="aspect-square">
|
||||
<div className="aspect-square bg-white p-2 flex items-center justify-center border-b border-gray-100">
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={media.filename}
|
||||
className="w-full h-full object-cover"
|
||||
className="max-w-full max-h-full object-contain object-center"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
|
||||
<button
|
||||
|
||||
@ -209,11 +209,11 @@ export default function MediaManager() {
|
||||
className="group relative bg-gray-100 rounded-xl overflow-hidden border border-gray-200 hover:border-purple-400 hover:shadow-lg hover:shadow-purple-100 transition-all cursor-pointer"
|
||||
onClick={() => setPreviewUrl(imgUrl)}
|
||||
>
|
||||
<div className="aspect-square">
|
||||
<div className="aspect-square bg-white p-2 flex items-center justify-center border-b border-gray-100">
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={media.filename}
|
||||
className="w-full h-full object-cover"
|
||||
className="max-w-full max-h-full object-contain object-center"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
|
||||
@ -172,6 +172,7 @@ export default function Products() {
|
||||
await api.post('/admin/products', finalPayload);
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setImageErrors({});
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('Save failed', error);
|
||||
@ -183,6 +184,7 @@ export default function Products() {
|
||||
if (!window.confirm('آیا از حذف این محصول مطمئن هستید؟')) return;
|
||||
try {
|
||||
await api.delete(`/admin/products/${id}`);
|
||||
setImageErrors({});
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('Delete failed', error);
|
||||
@ -261,12 +263,14 @@ export default function Products() {
|
||||
<tr key={product.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3 px-6">
|
||||
{product.imageUrl && !imageErrors[product.id] ? (
|
||||
<img
|
||||
src={getProductImageUrl(product.imageUrl)}
|
||||
alt=""
|
||||
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
|
||||
className="w-12 h-12 rounded-xl object-cover border border-gray-200"
|
||||
/>
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-50 border border-gray-200 p-1 flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
src={getProductImageUrl(product.imageUrl)}
|
||||
alt=""
|
||||
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
|
||||
className="max-w-full max-h-full object-contain object-center"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
|
||||
@ -29,7 +29,7 @@ export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const { items, getTotal, getSubtotal, getDiscount, isSubscribed, charityDonation, setCharityDonation, addOrder, clearCart } = useCartStore();
|
||||
const { getActivePet, updatePet } = usePetStore();
|
||||
const { profile, updateProfile } = useUserStore();
|
||||
const { profile, isLoggedIn, updateProfile } = useUserStore();
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [useRoundUp, setUseRoundUp] = useState(false);
|
||||
@ -48,15 +48,17 @@ export default function CheckoutPage() {
|
||||
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
||||
|
||||
// Set default selected address if logged in
|
||||
if (profile.addresses && profile.addresses.length > 0) {
|
||||
if (isLoggedIn && profile.addresses && profile.addresses.length > 0) {
|
||||
const def = profile.addresses.find(a => a.isDefault) || profile.addresses[0];
|
||||
setSelectedAddressId(def.id);
|
||||
} else if (profile.firstName) {
|
||||
} else if (isLoggedIn && profile.firstName) {
|
||||
// Pre-fill profile info for manual fields
|
||||
setManualName(`${profile.firstName} ${profile.lastName}`.trim());
|
||||
setManualPhone(profile.mobile || '');
|
||||
} else {
|
||||
setSelectedAddressId('');
|
||||
}
|
||||
}, [profile]);
|
||||
}, [profile, isLoggedIn]);
|
||||
|
||||
const subtotal = getSubtotal();
|
||||
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
||||
@ -210,7 +212,7 @@ export default function CheckoutPage() {
|
||||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||||
</div>
|
||||
|
||||
{profile.addresses && profile.addresses.length > 0 && !showNewAddressForm ? (
|
||||
{isLoggedIn && profile.addresses && profile.addresses.length > 0 && !showNewAddressForm ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs font-bold text-medical-gray-500 mb-2">انتخاب از بین آدرسهای ذخیرهشده شما:</p>
|
||||
<div className="grid gap-3">
|
||||
|
||||
@ -38,6 +38,12 @@ export class OrderService {
|
||||
*/
|
||||
public async createOrder(orderData: {
|
||||
petId?: string;
|
||||
couponCode?: string;
|
||||
prescriptionUrl?: string;
|
||||
charityDonation?: number;
|
||||
isRefill?: boolean;
|
||||
refillIntervalDays?: number;
|
||||
shippingAddress?: string;
|
||||
items: { productId: string; quantity: number }[];
|
||||
}): Promise<Order> {
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user