Explorar o código

后台订单列表补充地址、发票、交易号和商品规格。

Co-authored-by: Cursor <cursoragent@cursor.com>
chengwl hai 3 días
pai
achega
86799baccb

+ 50 - 0
packages/Webkul/BagistoApi/src/Admin/Dto/AdminOrderListDto.php

@@ -57,9 +57,31 @@ class AdminOrderListDto
     #[ApiProperty(writable: false)]
     public ?int $total_qty_ordered = null;
 
+    #[ApiProperty(writable: false)]
+    public ?string $base_currency_code = null;
+
+    #[ApiProperty(writable: false)]
+    public ?string $channel_currency_code = null;
+
     #[ApiProperty(writable: false)]
     public ?string $order_currency_code = null;
 
+    #[ApiProperty(writable: false)]
+    public ?string $transaction_id = null;
+
+    /** Latest note from `customer_notes` for this order's customer. */
+    #[ApiProperty(writable: false)]
+    public ?string $customer_note = null;
+
+    #[ApiProperty(writable: false)]
+    public ?float $shipping_insurance_amount = null;
+
+    #[ApiProperty(writable: false)]
+    public ?float $base_shipping_insurance_amount = null;
+
+    #[ApiProperty(writable: false)]
+    public ?string $formatted_shipping_insurance_amount = null;
+
     #[ApiProperty(writable: false)]
     public ?float $grand_total = null;
 
@@ -78,6 +100,34 @@ class AdminOrderListDto
     #[ApiProperty(writable: false)]
     public ?string $updated_at = null;
 
+    /**
+     * Billing + shipping addresses. Each row carries `addressType`.
+     *
+     * @var array<int, array<string, mixed>>
+     */
+    #[ApiProperty(writable: false)]
+    public array $addresses = [];
+
+    /** @var array<int, array<string, mixed>> */
+    #[ApiProperty(writable: false)]
+    public array $invoices = [];
+
+    /**
+     * Customer profile notes (`customer_notes`), newest first.
+     *
+     * @var array<int, array<string, mixed>>
+     */
+    #[ApiProperty(writable: false)]
+    public array $customer_notes = [];
+
+    /**
+     * Order comments, newest first.
+     *
+     * @var array<int, array<string, mixed>>
+     */
+    #[ApiProperty(writable: false)]
+    public array $comments = [];
+
     /** @var array<int, array<string, mixed>> */
     #[ApiProperty(writable: false)]
     public array $items = [];

+ 121 - 4
packages/Webkul/BagistoApi/src/Admin/Models/AdminOrder.php

@@ -21,8 +21,10 @@ use Webkul\BagistoApi\Admin\State\OrderCollectionProvider;
  *         (the `data`/`meta` envelope is applied by AdminCollectionEnvelopeNormalizer).
  * GraphQL: adminOrders query → native cursor pagination (edges + pageInfo).
  *
- * Only flat order fields + a light `items` preview. Heavy relations
- * (full items, invoices, shipments) are served by sub-resources.
+ * Flat order fields, a light `items` preview (variant, price, tax, discount),
+ * billing/shipping addresses, invoices (with transaction id), the order
+ * transaction id, customer notes, currency codes, and lost-package insurance.
+ * Full item / shipment / refund trees stay on the order detail.
  */
 #[ApiResource(
     routePrefix: '/api/admin',
@@ -37,7 +39,7 @@ use Webkul\BagistoApi\Admin\State\OrderCollectionProvider;
             openapi: new Model\Operation(
                 tags: ['Admin Sales: Orders'],
                 summary: 'List orders',
-                description: 'Paginated, filterable list of all orders across every customer. Returns a slim row per order in a `{ data, meta }` envelope; use the order detail and sub-resources for items / invoices / shipments.',
+                description: 'Paginated, filterable list of all orders across every customer. Each row includes addresses, invoices, transactionId, customer notes, currency codes, lost-package insurance, and an items preview (variant name/title, price, total, tax, discount, productId). Use the order detail for shipments and refunds.',
                 parameters: [
                     new Model\Parameter('page', 'query', 'Page number', false, schema: ['type' => 'integer', 'example' => 1]),
                     new Model\Parameter('per_page', 'query', 'Items per page (max 50)', false, schema: ['type' => 'integer', 'example' => 10]),
@@ -77,13 +79,65 @@ use Webkul\BagistoApi\Admin\State\OrderCollectionProvider;
                                             'couponCode' => null,
                                             'totalItemCount' => 1,
                                             'totalQtyOrdered' => 1,
+                                            'baseCurrencyCode' => 'USD',
+                                            'channelCurrencyCode' => 'USD',
                                             'orderCurrencyCode' => 'USD',
+                                            'transactionId' => 'CAP-123',
+                                            'customerNote' => 'Leave at the front desk.',
+                                            'shippingInsuranceAmount' => 1.99,
+                                            'baseShippingInsuranceAmount' => 1.99,
+                                            'formattedShippingInsuranceAmount' => '$1.99',
                                             'grandTotal' => 4000,
                                             'baseGrandTotal' => 4000,
                                             'formattedGrandTotal' => '$4,000.00',
                                             'location' => 'New York, NY, US',
                                             'createdAt' => '2026-05-19 13:13:29',
                                             'updatedAt' => '2026-05-19 13:13:30',
+                                            'addresses' => [
+                                                [
+                                                    'id' => 10,
+                                                    'addressType' => 'order_billing',
+                                                    'firstName' => 'Test',
+                                                    'lastName' => 'User',
+                                                    'companyName' => null,
+                                                    'vatId' => null,
+                                                    'address' => '1 Main St',
+                                                    'city' => 'New York',
+                                                    'state' => 'NY',
+                                                    'country' => 'US',
+                                                    'postcode' => '10001',
+                                                    'email' => 'admin@example.com',
+                                                    'phone' => '5550100',
+                                                ],
+                                            ],
+                                            'invoices' => [
+                                                [
+                                                    'id' => 80,
+                                                    'incrementId' => '80',
+                                                    'state' => 'paid',
+                                                    'emailSent' => true,
+                                                    'totalQty' => 1,
+                                                    'subTotal' => 4000,
+                                                    'grandTotal' => 4000,
+                                                    'taxAmount' => 0,
+                                                    'discountAmount' => 0,
+                                                    'shippingAmount' => 0,
+                                                    'shippingInsuranceAmount' => 1.99,
+                                                    'baseShippingInsuranceAmount' => 1.99,
+                                                    'transactionId' => 'CAP-123',
+                                                    'createdAt' => '2026-05-19 13:13:30',
+                                                ],
+                                            ],
+                                            'customerNotes' => [
+                                                [
+                                                    'id' => 3,
+                                                    'note' => 'Leave at the front desk.',
+                                                    'customerId' => 19,
+                                                    'customerNotified' => false,
+                                                    'createdAt' => '2026-05-19 12:00:00',
+                                                ],
+                                            ],
+                                            'comments' => [],
                                             'items' => [
                                                 [
                                                     'id' => 2694,
@@ -91,6 +145,19 @@ use Webkul\BagistoApi\Admin\State\OrderCollectionProvider;
                                                     'name' => 'Classic Watch Hand',
                                                     'qtyOrdered' => 1,
                                                     'productImage' => 'http://localhost:8000/storage/product/2358/example.webp',
+                                                    'productId' => 2358,
+                                                    'variantName' => 'Classic Watch Hand / Red',
+                                                    'variantTitle' => 'Red / M',
+                                                    'price' => 4000,
+                                                    'basePrice' => 4000,
+                                                    'formattedPrice' => '$4,000.00',
+                                                    'total' => 4000,
+                                                    'baseTotal' => 4000,
+                                                    'formattedTotal' => '$4,000.00',
+                                                    'taxAmount' => 0,
+                                                    'formattedTaxAmount' => '$0.00',
+                                                    'discountAmount' => 0,
+                                                    'formattedDiscountAmount' => '$0.00',
                                                 ],
                                             ],
                                         ],
@@ -159,7 +226,8 @@ class AdminOrder extends EloquentModel
 
     protected $appends = [
         'status_label', 'channel_name', 'customer_name', 'payment_title',
-        'location', 'formatted_grand_total',
+        'location', 'formatted_grand_total', 'transaction_id', 'customer_note',
+        'formatted_shipping_insurance_amount',
     ];
 
     protected $casts = [
@@ -171,6 +239,8 @@ class AdminOrder extends EloquentModel
         'total_qty_ordered' => 'int',
         'grand_total' => 'float',
         'base_grand_total' => 'float',
+        'shipping_insurance_amount' => 'float',
+        'base_shipping_insurance_amount' => 'float',
     ];
 
     #[ApiProperty(identifier: true, writable: false)]
@@ -185,6 +255,31 @@ class AdminOrder extends EloquentModel
         return $this->hasMany(AdminOrderItemPreview::class, 'order_id');
     }
 
+    #[ApiProperty(writable: false)]
+    public function addresses(): HasMany
+    {
+        return $this->hasMany(AdminOrderDetailAddress::class, 'order_id')
+            ->whereIn('address_type', ['order_billing', 'order_shipping']);
+    }
+
+    #[ApiProperty(writable: false)]
+    public function invoices(): HasMany
+    {
+        return $this->hasMany(AdminOrderDetailInvoice::class, 'order_id');
+    }
+
+    #[ApiProperty(writable: false)]
+    public function comments(): HasMany
+    {
+        return $this->hasMany(AdminOrderDetailComment::class, 'order_id')->orderByDesc('id');
+    }
+
+    #[ApiProperty(writable: false)]
+    public function customerNotes(): HasMany
+    {
+        return $this->hasMany(AdminOrderCustomerNote::class, 'customer_id', 'customer_id');
+    }
+
     #[ApiProperty(writable: false)]
     public function getStatusLabelAttribute(): ?string
     {
@@ -220,4 +315,26 @@ class AdminOrder extends EloquentModel
     {
         return $this->attributes['formatted_grand_total'] ?? null;
     }
+
+    #[ApiProperty(writable: false)]
+    public function getTransactionIdAttribute(): ?string
+    {
+        $value = $this->attributes['transaction_id'] ?? null;
+
+        return $value === null || $value === '' ? null : (string) $value;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getCustomerNoteAttribute(): ?string
+    {
+        $value = $this->attributes['customer_note'] ?? null;
+
+        return $value === null || $value === '' ? null : (string) $value;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getFormattedShippingInsuranceAmountAttribute(): ?string
+    {
+        return $this->attributes['formatted_shipping_insurance_amount'] ?? null;
+    }
 }

+ 37 - 0
packages/Webkul/BagistoApi/src/Admin/Models/AdminOrderCustomerNote.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Webkul\BagistoApi\Admin\Models;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * Customer profile note — nested on the admin orders listing (`customerNotes`).
+ * Rows come from `customer_notes` (the admin customer "备注" panel).
+ */
+#[ApiResource(
+    shortName: 'AdminOrderCustomerNote',
+    operations: [],
+    graphQlOperations: [],
+    normalizationContext: ['attributes' => ['id', 'note', 'customer_notified', 'created_at']],
+)]
+class AdminOrderCustomerNote extends Model
+{
+    /** @var string */
+    protected $table = 'customer_notes';
+
+    /** @var array */
+    protected $casts = [
+        'id' => 'int',
+        'customer_id' => 'int',
+        'customer_notified' => 'boolean',
+        'created_at' => 'datetime',
+    ];
+
+    #[ApiProperty(identifier: true, writable: false)]
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+}

+ 3 - 0
packages/Webkul/BagistoApi/src/Admin/Models/AdminOrderDetailInvoice.php

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Model;
     normalizationContext: ['attributes' => [
         'id', 'increment_id', 'state', 'email_sent', 'total_qty',
         'sub_total', 'grand_total', 'tax_amount', 'discount_amount', 'shipping_amount',
+        'shipping_insurance_amount', 'base_shipping_insurance_amount',
         'transaction_id', 'created_at',
     ]],
 )]
@@ -35,6 +36,8 @@ class AdminOrderDetailInvoice extends Model
         'tax_amount' => 'float',
         'discount_amount' => 'float',
         'shipping_amount' => 'float',
+        'shipping_insurance_amount' => 'float',
+        'base_shipping_insurance_amount' => 'float',
         'created_at' => 'datetime',
     ];
 

+ 131 - 3
packages/Webkul/BagistoApi/src/Admin/Models/AdminOrderItemPreview.php

@@ -19,7 +19,14 @@ use Illuminate\Support\Facades\Storage;
     shortName: 'AdminOrderItemPreview',
     operations: [],
     graphQlOperations: [],
-    normalizationContext: ['attributes' => ['id', 'sku', 'name', 'qty_ordered', 'product_image']],
+    normalizationContext: ['attributes' => [
+        'id', 'sku', 'name', 'qty_ordered', 'product_image', 'product_id',
+        'variant_name', 'variant_title',
+        'price', 'base_price', 'formatted_price',
+        'total', 'base_total', 'formatted_total',
+        'tax_amount', 'formatted_tax_amount',
+        'discount_amount', 'formatted_discount_amount',
+    ]],
 )]
 class AdminOrderItemPreview extends Model
 {
@@ -27,10 +34,24 @@ class AdminOrderItemPreview extends Model
     protected $table = 'order_items';
 
     /** @var array */
-    protected $appends = ['product_image'];
+    protected $appends = [
+        'product_image', 'variant_name', 'variant_title',
+        'formatted_price', 'formatted_total', 'formatted_tax_amount', 'formatted_discount_amount',
+    ];
 
     /** @var array */
-    protected $casts = ['id' => 'int', 'qty_ordered' => 'int'];
+    protected $casts = [
+        'id' => 'int',
+        'product_id' => 'int',
+        'qty_ordered' => 'int',
+        'price' => 'float',
+        'base_price' => 'float',
+        'total' => 'float',
+        'base_total' => 'float',
+        'tax_amount' => 'float',
+        'discount_amount' => 'float',
+        'additional' => 'array',
+    ];
 
     #[ApiProperty(identifier: true, writable: false)]
     public function getId(): ?int
@@ -54,4 +75,111 @@ class AdminOrderItemPreview extends Model
 
         return $path ? Storage::url($path) : null;
     }
+
+    #[ApiProperty(writable: false)]
+    public function getVariantNameAttribute(): ?string
+    {
+        if (array_key_exists('variant_name', $this->attributes)) {
+            $value = $this->attributes['variant_name'];
+
+            return $value === null || $value === '' ? null : (string) $value;
+        }
+
+        return self::variantNameFrom($this->additionalOptions(), null);
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getVariantTitleAttribute(): ?string
+    {
+        if (array_key_exists('variant_title', $this->attributes)) {
+            $value = $this->attributes['variant_title'];
+
+            return $value === null || $value === '' ? null : (string) $value;
+        }
+
+        return self::variantTitleFrom($this->additionalOptions());
+    }
+
+    protected function additionalOptions(): ?array
+    {
+        $additional = $this->additional;
+
+        return is_array($additional) ? $additional : null;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getFormattedPriceAttribute(): ?string
+    {
+        return $this->attributes['formatted_price'] ?? null;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getFormattedTotalAttribute(): ?string
+    {
+        return $this->attributes['formatted_total'] ?? null;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getFormattedTaxAmountAttribute(): ?string
+    {
+        return $this->attributes['formatted_tax_amount'] ?? null;
+    }
+
+    #[ApiProperty(writable: false)]
+    public function getFormattedDiscountAmountAttribute(): ?string
+    {
+        return $this->attributes['formatted_discount_amount'] ?? null;
+    }
+
+    /**
+     * Variant display name stored on the line (`additional.variant_name`),
+     * otherwise the configurable child product name.
+     */
+    public static function variantNameFrom(?array $additional, ?string $childName): ?string
+    {
+        $name = trim((string) ($additional['variant_name'] ?? ''));
+
+        if ($name !== '') {
+            return $name;
+        }
+
+        $childName = trim((string) $childName);
+
+        return $childName !== '' ? $childName : null;
+    }
+
+    /**
+     * Variant option title. Prefers `additional.variant_title`, otherwise joins
+     * selected option labels (`value_label`, then `option_label`).
+     */
+    public static function variantTitleFrom(?array $additional): ?string
+    {
+        $explicit = trim((string) ($additional['variant_title'] ?? ''));
+
+        if ($explicit !== '') {
+            return $explicit;
+        }
+
+        $attributes = $additional['attributes'] ?? null;
+
+        if (! is_array($attributes) || $attributes === []) {
+            return null;
+        }
+
+        $labels = [];
+
+        foreach ($attributes as $attribute) {
+            if (! is_array($attribute)) {
+                continue;
+            }
+
+            $label = trim((string) ($attribute['value_label'] ?? $attribute['option_label'] ?? ''));
+
+            if ($label !== '') {
+                $labels[] = $label;
+            }
+        }
+
+        return $labels !== [] ? implode(' / ', $labels) : null;
+    }
 }

+ 325 - 25
packages/Webkul/BagistoApi/src/Admin/State/OrderCollectionProvider.php

@@ -6,10 +6,16 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProviderInterface;
 use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Storage;
 use Webkul\BagistoApi\Admin\Dto\AdminOrderListDto;
 use Webkul\BagistoApi\Admin\Helper\AdminAuthHelper;
 use Webkul\BagistoApi\Admin\Models\AdminOrder;
+use Webkul\BagistoApi\Admin\Models\AdminOrderCustomerNote;
+use Webkul\BagistoApi\Admin\Models\AdminOrderDetailAddress;
+use Webkul\BagistoApi\Admin\Models\AdminOrderDetailComment;
+use Webkul\BagistoApi\Admin\Models\AdminOrderDetailInvoice;
 use Webkul\BagistoApi\Admin\Models\AdminOrderItemPreview;
 use Webkul\BagistoApi\Admin\State\Concerns\ResolvesAdminDateRange;
 use Webkul\BagistoApi\Exception\AuthenticationException;
@@ -45,8 +51,12 @@ class OrderCollectionProvider implements ProviderInterface
 
         $query = Order::query()->with([
             'items.product.images',
+            'items.child',
             'addresses',
             'payment',
+            'invoices',
+            'transactions',
+            'comments',
         ]);
 
         $this->applyFilters($query, $args);
@@ -56,12 +66,16 @@ class OrderCollectionProvider implements ProviderInterface
 
         $isGraphQL = ! empty($context['graphql_operation_name']);
 
-        $orders = $query->offset(($page - 1) * $perPage)
+        $orderModels = $query->offset(($page - 1) * $perPage)
             ->limit($perPage)
-            ->get()
+            ->get();
+
+        $notesByCustomer = $this->customerNotesByCustomer($orderModels);
+
+        $orders = $orderModels
             ->map(fn (Order $order) => $isGraphQL
-                ? $this->toAdminOrderEloquent($order)
-                : $this->toAdminOrderDto($order))
+                ? $this->toAdminOrderEloquent($order, $notesByCustomer)
+                : $this->toAdminOrderDto($order, $notesByCustomer))
             ->all();
 
         return new Paginator(
@@ -177,8 +191,16 @@ class OrderCollectionProvider implements ProviderInterface
     /**
      * Map an Order model to the slim AdminOrder row.
      */
-    protected function toAdminOrderEloquent(Order $order): AdminOrder
+    /**
+     * @param  array<int, array<int, array<string, mixed>>>  $notesByCustomer
+     */
+    protected function toAdminOrderEloquent(Order $order, array $notesByCustomer = []): AdminOrder
     {
+        $currency = $order->order_currency_code;
+        $notes = $this->notesFor($order, $notesByCustomer);
+        $insurance = (float) ($order->shipping_insurance_amount ?? 0);
+        $baseInsurance = (float) ($order->base_shipping_insurance_amount ?? 0);
+
         $model = (new AdminOrder)->forceFill(array_merge($order->getAttributes(), [
             'id' => (int) $order->id,
             'increment_id' => $order->increment_id,
@@ -194,34 +216,53 @@ class OrderCollectionProvider implements ProviderInterface
             'coupon_code' => $order->coupon_code,
             'total_item_count' => $order->total_item_count,
             'total_qty_ordered' => (int) $order->total_qty_ordered,
-            'order_currency_code' => $order->order_currency_code,
+            'base_currency_code' => $order->base_currency_code,
+            'channel_currency_code' => $order->channel_currency_code,
+            'order_currency_code' => $currency,
+            'transaction_id' => $this->orderTransactionId($order),
+            'customer_note' => $notes[0]['note'] ?? null,
+            'shipping_insurance_amount' => $insurance,
+            'base_shipping_insurance_amount' => $baseInsurance,
+            'formatted_shipping_insurance_amount' => $this->safeFormatPrice($insurance, $currency),
             'grand_total' => (float) $order->grand_total,
             'base_grand_total' => (float) $order->base_grand_total,
-            'formatted_grand_total' => $this->safeFormatPrice($order->grand_total, $order->order_currency_code),
+            'formatted_grand_total' => $this->safeFormatPrice($order->grand_total, $currency),
             'location' => $this->billingLocation($order),
             'created_at' => (string) $order->created_at,
             'updated_at' => (string) $order->updated_at,
         ]));
 
-        $items = $order->items->map(function ($orderItem) {
-            $preview = $this->toItemPreview($orderItem);
-
-            return (new AdminOrderItemPreview)->forceFill([
-                'id' => $preview['id'],
-                'sku' => $preview['sku'],
-                'name' => $preview['name'],
-                'qty_ordered' => $preview['qtyOrdered'],
-                'product_image' => $preview['productImage'],
-            ]);
-        })->values();
-
-        $model->setRelation('items', $items);
+        $model->setRelation('items', $order->items->map(
+            fn ($orderItem) => $this->toItemModel($orderItem, $currency)
+        )->values());
+        $model->setRelation('addresses', $this->addressModels($order));
+        $model->setRelation('invoices', $order->invoices->map(
+            fn ($invoice) => $this->toInvoiceModel($invoice, $order)
+        )->values());
+        $model->setRelation('comments', $this->commentModels($order));
+        $model->setRelation('customerNotes', collect($notes)->map(
+            fn (array $note) => (new AdminOrderCustomerNote)->forceFill([
+                'id' => $note['id'],
+                'customer_id' => $note['customerId'],
+                'note' => $note['note'],
+                'customer_notified' => $note['customerNotified'],
+                'created_at' => $note['createdAt'],
+            ])
+        )->values());
 
         return $model;
     }
 
-    protected function toAdminOrderDto(Order $order): AdminOrderListDto
+    /**
+     * @param  array<int, array<int, array<string, mixed>>>  $notesByCustomer
+     */
+    protected function toAdminOrderDto(Order $order, array $notesByCustomer = []): AdminOrderListDto
     {
+        $currency = $order->order_currency_code;
+        $notes = $this->notesFor($order, $notesByCustomer);
+        $insurance = (float) ($order->shipping_insurance_amount ?? 0);
+        $baseInsurance = (float) ($order->base_shipping_insurance_amount ?? 0);
+
         $row = new AdminOrderListDto;
 
         $row->id = $order->id;
@@ -238,14 +279,32 @@ class OrderCollectionProvider implements ProviderInterface
         $row->coupon_code = $order->coupon_code;
         $row->total_item_count = $order->total_item_count;
         $row->total_qty_ordered = (int) $order->total_qty_ordered;
-        $row->order_currency_code = $order->order_currency_code;
+        $row->base_currency_code = $order->base_currency_code;
+        $row->channel_currency_code = $order->channel_currency_code;
+        $row->order_currency_code = $currency;
+        $row->transaction_id = $this->orderTransactionId($order);
+        $row->customer_note = $notes[0]['note'] ?? null;
+        $row->shipping_insurance_amount = $insurance;
+        $row->base_shipping_insurance_amount = $baseInsurance;
+        $row->formatted_shipping_insurance_amount = $this->safeFormatPrice($insurance, $currency);
         $row->grand_total = (float) $order->grand_total;
         $row->base_grand_total = (float) $order->base_grand_total;
-        $row->formatted_grand_total = $this->safeFormatPrice($order->grand_total, $order->order_currency_code);
+        $row->formatted_grand_total = $this->safeFormatPrice($order->grand_total, $currency);
         $row->location = $this->billingLocation($order);
         $row->created_at = (string) $order->created_at;
         $row->updated_at = (string) $order->updated_at;
-        $row->items = $order->items->map(fn ($orderItem) => $this->toItemPreview($orderItem))->all();
+        $row->addresses = array_values(array_filter([
+            $this->toAddress($order->billing_address),
+            $this->toAddress($order->shipping_address),
+        ]));
+        $row->invoices = $order->invoices
+            ->map(fn ($invoice) => $this->toInvoice($invoice, $order, $currency))
+            ->all();
+        $row->customer_notes = $notes;
+        $row->comments = $this->toComments($order);
+        $row->items = $order->items
+            ->map(fn ($orderItem) => $this->toItemPreview($orderItem, $currency))
+            ->all();
 
         return $row;
     }
@@ -264,9 +323,11 @@ class OrderCollectionProvider implements ProviderInterface
         }
     }
 
-    protected function toItemPreview($orderItem): array
+    protected function toItemPreview($orderItem, ?string $currency): array
     {
         $image = $orderItem->product?->images?->first();
+        $additional = is_array($orderItem->additional) ? $orderItem->additional : null;
+        $childName = $orderItem->relationLoaded('child') ? $orderItem->child?->name : null;
 
         return [
             'id' => $orderItem->id,
@@ -274,9 +335,248 @@ class OrderCollectionProvider implements ProviderInterface
             'name' => $orderItem->name,
             'qtyOrdered' => (int) $orderItem->qty_ordered,
             'productImage' => $image ? Storage::url($image->path) : null,
+            'productId' => $orderItem->product_id !== null ? (int) $orderItem->product_id : null,
+            'variantName' => AdminOrderItemPreview::variantNameFrom($additional, $childName),
+            'variantTitle' => AdminOrderItemPreview::variantTitleFrom($additional),
+            'price' => (float) $orderItem->price,
+            'basePrice' => (float) $orderItem->base_price,
+            'formattedPrice' => $this->safeFormatPrice($orderItem->price, $currency),
+            'total' => (float) $orderItem->total,
+            'baseTotal' => (float) $orderItem->base_total,
+            'formattedTotal' => $this->safeFormatPrice($orderItem->total, $currency),
+            'taxAmount' => (float) $orderItem->tax_amount,
+            'formattedTaxAmount' => $this->safeFormatPrice($orderItem->tax_amount, $currency),
+            'discountAmount' => (float) $orderItem->discount_amount,
+            'formattedDiscountAmount' => $this->safeFormatPrice($orderItem->discount_amount, $currency),
+        ];
+    }
+
+    protected function toItemModel($orderItem, ?string $currency): AdminOrderItemPreview
+    {
+        $preview = $this->toItemPreview($orderItem, $currency);
+
+        return (new AdminOrderItemPreview)->forceFill([
+            'id' => $preview['id'],
+            'sku' => $preview['sku'],
+            'name' => $preview['name'],
+            'qty_ordered' => $preview['qtyOrdered'],
+            'product_image' => $preview['productImage'],
+            'product_id' => $preview['productId'],
+            'variant_name' => $preview['variantName'],
+            'variant_title' => $preview['variantTitle'],
+            'price' => $preview['price'],
+            'base_price' => $preview['basePrice'],
+            'formatted_price' => $preview['formattedPrice'],
+            'total' => $preview['total'],
+            'base_total' => $preview['baseTotal'],
+            'formatted_total' => $preview['formattedTotal'],
+            'tax_amount' => $preview['taxAmount'],
+            'formatted_tax_amount' => $preview['formattedTaxAmount'],
+            'discount_amount' => $preview['discountAmount'],
+            'formatted_discount_amount' => $preview['formattedDiscountAmount'],
+        ]);
+    }
+
+    protected function toAddress($address): ?array
+    {
+        if (! $address) {
+            return null;
+        }
+
+        return [
+            'id' => (int) $address->id,
+            'addressType' => $address->address_type,
+            'firstName' => $address->first_name,
+            'lastName' => $address->last_name,
+            'companyName' => $address->company_name,
+            'vatId' => $address->vat_id,
+            'address' => $address->address,
+            'city' => $address->city,
+            'state' => $address->state,
+            'country' => $address->country,
+            'postcode' => $address->postcode,
+            'email' => $address->email,
+            'phone' => $address->phone,
         ];
     }
 
+    protected function addressModels(Order $order): Collection
+    {
+        return collect(array_filter([
+            $order->billing_address,
+            $order->shipping_address,
+        ]))->map(function ($address) {
+            return (new AdminOrderDetailAddress)->forceFill($address->getAttributes());
+        })->values();
+    }
+
+    protected function toInvoice($invoice, Order $order, ?string $currency): array
+    {
+        $insurance = (float) ($invoice->shipping_insurance_amount ?? 0);
+        $baseInsurance = (float) ($invoice->base_shipping_insurance_amount ?? 0);
+
+        return [
+            'id' => (int) $invoice->id,
+            'incrementId' => $invoice->increment_id,
+            'state' => $invoice->state,
+            'emailSent' => (bool) $invoice->email_sent,
+            'totalQty' => (int) $invoice->total_qty,
+            'subTotal' => (float) $invoice->sub_total,
+            'formattedSubTotal' => $this->safeFormatPrice($invoice->sub_total, $currency),
+            'grandTotal' => (float) $invoice->grand_total,
+            'formattedGrandTotal' => $this->safeFormatPrice($invoice->grand_total, $currency),
+            'taxAmount' => (float) $invoice->tax_amount,
+            'discountAmount' => (float) $invoice->discount_amount,
+            'shippingAmount' => (float) $invoice->shipping_amount,
+            'shippingInsuranceAmount' => $insurance,
+            'baseShippingInsuranceAmount' => $baseInsurance,
+            'transactionId' => $this->invoiceTransactionId($invoice, $order),
+            'createdAt' => (string) $invoice->created_at,
+        ];
+    }
+
+    protected function toInvoiceModel($invoice, Order $order): AdminOrderDetailInvoice
+    {
+        return (new AdminOrderDetailInvoice)->forceFill(array_merge($invoice->getAttributes(), [
+            'transaction_id' => $this->invoiceTransactionId($invoice, $order),
+            'shipping_insurance_amount' => (float) ($invoice->shipping_insurance_amount ?? 0),
+            'base_shipping_insurance_amount' => (float) ($invoice->base_shipping_insurance_amount ?? 0),
+        ]));
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected function toComments(Order $order): array
+    {
+        return $order->comments
+            ->sortByDesc('id')
+            ->map(fn ($comment) => [
+                'id' => (int) $comment->id,
+                'comment' => $comment->comment,
+                'customerNotified' => (bool) $comment->customer_notified,
+                'createdAt' => (string) $comment->created_at,
+            ])
+            ->values()
+            ->all();
+    }
+
+    protected function commentModels(Order $order): Collection
+    {
+        return $order->comments
+            ->sortByDesc('id')
+            ->map(fn ($comment) => (new AdminOrderDetailComment)->forceFill([
+                'id' => (int) $comment->id,
+                'comment' => $comment->comment,
+                'customer_notified' => (bool) $comment->customer_notified,
+                'created_at' => $comment->created_at,
+            ]))
+            ->values();
+    }
+
+    /**
+     * Gateway capture id for the order. Payment additional is written on
+     * capture (`transaction_id`) and by the Magento import (`last_trans_id`);
+     * invoices and `order_transactions` are the fallback.
+     */
+    protected function orderTransactionId(Order $order): ?string
+    {
+        $additional = $order->payment?->additional;
+
+        if (is_array($additional)) {
+            foreach (['transaction_id', 'last_trans_id'] as $key) {
+                $value = trim((string) ($additional[$key] ?? ''));
+
+                if ($value !== '') {
+                    return $value;
+                }
+            }
+
+            $paypal = $additional['paypal']['Last Transaction ID'] ?? null;
+
+            if (is_string($paypal) && trim($paypal) !== '') {
+                return trim($paypal);
+            }
+        }
+
+        foreach ($order->invoices as $invoice) {
+            $id = $this->invoiceTransactionId($invoice, $order);
+
+            if ($id !== null) {
+                return $id;
+            }
+        }
+
+        $txn = $order->transactions->first();
+
+        if ($txn && ! empty($txn->transaction_id)) {
+            return (string) $txn->transaction_id;
+        }
+
+        return null;
+    }
+
+    protected function invoiceTransactionId($invoice, Order $order): ?string
+    {
+        if (! empty($invoice->transaction_id)) {
+            return (string) $invoice->transaction_id;
+        }
+
+        $linked = $order->transactions->first(
+            fn ($txn) => (int) $txn->invoice_id === (int) $invoice->id
+        );
+
+        if ($linked && ! empty($linked->transaction_id)) {
+            return (string) $linked->transaction_id;
+        }
+
+        return null;
+    }
+
+    /**
+     * Customer profile notes, newest first, keyed by customer id.
+     *
+     * @param  Collection<int, Order>  $orders
+     * @return array<int, array<int, array<string, mixed>>>
+     */
+    protected function customerNotesByCustomer(Collection $orders): array
+    {
+        $ids = $orders->pluck('customer_id')->filter()->map(fn ($id) => (int) $id)->unique()->values();
+
+        if ($ids->isEmpty()) {
+            return [];
+        }
+
+        $grouped = [];
+
+        foreach (DB::table('customer_notes')->whereIn('customer_id', $ids->all())->orderByDesc('id')->get() as $row) {
+            $grouped[(int) $row->customer_id][] = [
+                'id' => (int) $row->id,
+                'note' => $row->note,
+                'customerId' => (int) $row->customer_id,
+                'customerNotified' => (bool) $row->customer_notified,
+                'createdAt' => $row->created_at !== null ? (string) $row->created_at : null,
+            ];
+        }
+
+        return $grouped;
+    }
+
+    /**
+     * @param  array<int, array<int, array<string, mixed>>>  $notesByCustomer
+     * @return array<int, array<string, mixed>>
+     */
+    protected function notesFor(Order $order, array $notesByCustomer): array
+    {
+        $customerId = (int) ($order->customer_id ?? 0);
+
+        if ($customerId <= 0) {
+            return [];
+        }
+
+        return $notesByCustomer[$customerId] ?? [];
+    }
+
     /**
      * Resolve the payment-method display title from core config.
      */

+ 257 - 0
packages/Webkul/BagistoApi/tests/Unit/Admin/OrderCollectionProviderListTest.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace Webkul\BagistoApi\Tests\Unit\Admin;
+
+use Tests\TestCase;
+use Webkul\BagistoApi\Admin\State\OrderCollectionProvider;
+use Webkul\Sales\Models\Invoice;
+use Webkul\Sales\Models\Order;
+use Webkul\Sales\Models\OrderAddress;
+use Webkul\Sales\Models\OrderComment;
+use Webkul\Sales\Models\OrderItem;
+use Webkul\Sales\Models\OrderPayment;
+use Webkul\Sales\Models\OrderTransaction;
+
+class OrderCollectionProviderListTest extends TestCase
+{
+    public function test_list_row_includes_address_invoice_transaction_note_currency_insurance_and_variant(): void
+    {
+        $order = $this->order();
+
+        $dto = (new OrderListProbe)->map($order, [
+            9 => [[
+                'id' => 3,
+                'note' => 'VIP customer',
+                'customerId' => 9,
+                'customerNotified' => false,
+                'createdAt' => '2026-09-01 00:00:00',
+            ]],
+        ]);
+
+        $this->assertSame('USD', $dto->base_currency_code);
+        $this->assertSame('EUR', $dto->channel_currency_code);
+        $this->assertSame('EUR', $dto->order_currency_code);
+        $this->assertSame('CAP-9', $dto->transaction_id);
+        $this->assertSame('VIP customer', $dto->customer_note);
+        $this->assertSame('VIP customer', $dto->customer_notes[0]['note']);
+        $this->assertEquals(1.5, $dto->shipping_insurance_amount);
+        $this->assertEquals(1.6, $dto->base_shipping_insurance_amount);
+
+        $this->assertSame('order_billing', $dto->addresses[0]['addressType']);
+        $this->assertSame('London', $dto->addresses[0]['city']);
+        $this->assertSame('order_shipping', $dto->addresses[1]['addressType']);
+        $this->assertSame('Paris', $dto->addresses[1]['city']);
+
+        $this->assertSame('TXN-FROM-TABLE', $dto->invoices[0]['transactionId']);
+        $this->assertEquals(1.5, $dto->invoices[0]['shippingInsuranceAmount']);
+        $this->assertSame('Please gift wrap.', $dto->comments[0]['comment']);
+
+        $line = $dto->items[0];
+        $this->assertSame(88, $line['productId']);
+        $this->assertSame('Classic Red', $line['variantName']);
+        $this->assertSame('Red / M', $line['variantTitle']);
+        $this->assertEquals(40.0, $line['price']);
+        $this->assertEquals(80.0, $line['total']);
+        $this->assertEquals(4.0, $line['taxAmount']);
+        $this->assertEquals(5.0, $line['discountAmount']);
+    }
+
+    public function test_transaction_falls_back_and_variant_title_can_come_from_the_child(): void
+    {
+        $child = new OrderItem;
+        $child->forceFill(['name' => 'Child Variant']);
+
+        $item = new OrderItem;
+        $item->forceFill([
+            'id' => 12,
+            'sku' => 'WATCH-GOLD',
+            'name' => 'Classic Watch',
+            'product_id' => 89,
+            'qty_ordered' => 1,
+            'price' => 10,
+            'base_price' => 10,
+            'total' => 10,
+            'base_total' => 10,
+            'tax_amount' => 0,
+            'discount_amount' => 0,
+            'additional' => [
+                'variant_title' => 'Gold / Large',
+                'attributes' => [
+                    ['attribute_name' => 'Color', 'option_label' => 'Ignored'],
+                ],
+            ],
+        ]);
+        $item->setRelation('product', null);
+        $item->setRelation('child', $child);
+
+        $order = $this->order();
+        $order->payment->additional = ['last_trans_id' => 'LAST-1'];
+        $order->setRelation('items', collect([$item]));
+
+        $invoice = $order->invoices->first();
+        $invoice->transaction_id = 'INV-TXN';
+
+        $dto = (new OrderListProbe)->map($order, []);
+
+        $this->assertSame('LAST-1', $dto->transaction_id);
+        $this->assertSame('INV-TXN', $dto->invoices[0]['transactionId']);
+        $this->assertNull($dto->customer_note);
+        $this->assertSame([], $dto->customer_notes);
+        $this->assertSame('Child Variant', $dto->items[0]['variantName']);
+        $this->assertSame('Gold / Large', $dto->items[0]['variantTitle']);
+
+        $model = (new OrderListProbe)->mapEloquent($order, []);
+        $this->assertSame('LAST-1', $model->transaction_id);
+        $this->assertSame('Child Variant', $model->items->first()->variant_name);
+        $this->assertSame('Gold / Large', $model->items->first()->variant_title);
+        $this->assertSame(89, $model->items->first()->product_id);
+    }
+
+    private function order(): Order
+    {
+        $item = new OrderItem;
+        $item->forceFill([
+            'id' => 11,
+            'sku' => 'WATCH-RED',
+            'name' => 'Classic Watch',
+            'product_id' => 88,
+            'qty_ordered' => 2,
+            'price' => 40,
+            'base_price' => 40,
+            'total' => 80,
+            'base_total' => 80,
+            'tax_amount' => 4,
+            'discount_amount' => 5,
+            'additional' => [
+                'variant_name' => 'Classic Red',
+                'attributes' => [
+                    'color' => ['value_label' => 'Red', 'option_label' => 'Color'],
+                    'size' => ['option_label' => 'M'],
+                ],
+            ],
+        ]);
+        $item->setRelation('product', null);
+        $item->setRelation('child', null);
+
+        $billing = new OrderAddress;
+        $billing->forceFill([
+            'id' => 1,
+            'address_type' => OrderAddress::ADDRESS_TYPE_BILLING,
+            'first_name' => 'Ada',
+            'last_name' => 'Lovelace',
+            'company_name' => null,
+            'vat_id' => null,
+            'address' => '1 Main',
+            'city' => 'London',
+            'state' => 'LN',
+            'country' => 'GB',
+            'postcode' => 'EC1',
+            'email' => 'ada@example.com',
+            'phone' => '123',
+        ]);
+
+        $shipping = new OrderAddress;
+        $shipping->forceFill([
+            'id' => 2,
+            'address_type' => OrderAddress::ADDRESS_TYPE_SHIPPING,
+            'first_name' => 'Ada',
+            'last_name' => 'Lovelace',
+            'company_name' => null,
+            'vat_id' => null,
+            'address' => '2 Rue',
+            'city' => 'Paris',
+            'state' => 'IDF',
+            'country' => 'FR',
+            'postcode' => '75001',
+            'email' => 'ada@example.com',
+            'phone' => '456',
+        ]);
+
+        $payment = new OrderPayment;
+        $payment->forceFill([
+            'method' => 'paypal_standard',
+            'additional' => ['transaction_id' => 'CAP-9'],
+        ]);
+
+        $invoice = new Invoice;
+        $invoice->forceFill([
+            'id' => 7,
+            'increment_id' => '7',
+            'state' => 'paid',
+            'email_sent' => 1,
+            'total_qty' => 2,
+            'sub_total' => 80,
+            'grand_total' => 81.5,
+            'tax_amount' => 4,
+            'discount_amount' => 5,
+            'shipping_amount' => 2,
+            'shipping_insurance_amount' => 1.5,
+            'base_shipping_insurance_amount' => 1.5,
+            'transaction_id' => null,
+            'created_at' => '2026-10-01 00:00:00',
+        ]);
+
+        $txn = new OrderTransaction;
+        $txn->forceFill([
+            'id' => 3,
+            'invoice_id' => 7,
+            'transaction_id' => 'TXN-FROM-TABLE',
+            'order_id' => 5,
+        ]);
+
+        $comment = new OrderComment;
+        $comment->forceFill([
+            'id' => 4,
+            'comment' => 'Please gift wrap.',
+            'customer_notified' => 1,
+            'created_at' => '2026-10-01 01:00:00',
+        ]);
+
+        $order = new Order;
+        $order->forceFill([
+            'id' => 5,
+            'increment_id' => '1005',
+            'status' => Order::STATUS_PROCESSING,
+            'channel_id' => 1,
+            'channel_name' => 'Store',
+            'is_guest' => 0,
+            'customer_id' => 9,
+            'customer_email' => 'ada@example.com',
+            'customer_first_name' => 'Ada',
+            'customer_last_name' => 'Lovelace',
+            'coupon_code' => null,
+            'total_item_count' => 1,
+            'total_qty_ordered' => 2,
+            'base_currency_code' => 'USD',
+            'channel_currency_code' => 'EUR',
+            'order_currency_code' => 'EUR',
+            'grand_total' => 81.5,
+            'base_grand_total' => 90,
+            'shipping_insurance_amount' => 1.5,
+            'base_shipping_insurance_amount' => 1.6,
+            'created_at' => '2026-10-01 00:00:00',
+            'updated_at' => '2026-10-01 00:00:01',
+        ]);
+        $order->setRelation('items', collect([$item]));
+        $order->setRelation('addresses', collect([$billing, $shipping]));
+        $order->setRelation('payment', $payment);
+        $order->setRelation('invoices', collect([$invoice]));
+        $order->setRelation('transactions', collect([$txn]));
+        $order->setRelation('comments', collect([$comment]));
+
+        return $order;
+    }
+}
+
+class OrderListProbe extends OrderCollectionProvider
+{
+    public function map(Order $order, array $notes = [])
+    {
+        return $this->toAdminOrderDto($order, $notes);
+    }
+
+    public function mapEloquent(Order $order, array $notes = [])
+    {
+        return $this->toAdminOrderEloquent($order, $notes);
+    }
+}