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:
parsa aghaei 2026-07-26 20:11:26 +03:30
parent 989f77e106
commit 4979d4120c
9 changed files with 76 additions and 21 deletions

View File

@ -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": {

View File

@ -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()

View File

@ -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 })

View File

@ -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: {

View File

@ -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

View File

@ -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">

View File

@ -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" />

View File

@ -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">

View File

@ -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 {