Overview
The supplier catalog system allows suppliers to offer products to buyer tenants with flexible pricing, minimum order quantities, and visibility controls.
Adding Products to Catalog
Suppliers add existing products to their catalog with wholesale pricing:
use App\Services\ SupplierService ;
$catalogItem = $supplierService -> addToCatalog (
$supplierTenant ,
$product ,
[
'is_available' => true ,
'base_wholesale_price' => 45.00 ,
'min_order_quantity' => 10 ,
'visibility' => CatalogVisibility :: CONNECTIONS_ONLY ,
'description' => 'Bulk pricing available for orders over 50 units' ,
'pricing_tiers' => [
[ 'min_quantity' => 10 , 'max_quantity' => 49 , 'price' => 45.00 ],
[ 'min_quantity' => 50 , 'max_quantity' => 99 , 'price' => 42.00 ],
[ 'min_quantity' => 100 , 'max_quantity' => null , 'price' => 38.00 ]
]
]
);
See: app/Services/SupplierService.php:75
The base_wholesale_price is used when no pricing tier matches the order quantity.
Catalog Visibility Modes
Control who can see catalog items using the CatalogVisibility enum:
Mode Description Use Case PUBLICVisible to all tenants Open marketplace items CONNECTIONS_ONLYOnly visible to approved buyers Exclusive supplier relationships
use App\Enums\ CatalogVisibility ;
$catalogItem -> update ([
'visibility' => CatalogVisibility :: PUBLIC
]);
Even with PUBLIC visibility, buyers must have an active connection (APPROVED or ACTIVE status) to place orders.
Pricing Tiers
Pricing tiers enable volume-based discounts with two types:
General Pricing Tiers
Apply to all buyers:
$supplierService -> addPricingTier ( $catalogItem , [
'min_quantity' => 50 ,
'max_quantity' => 99 ,
'price' => 42.00
], null ); // null = no specific connection
Connection-Specific Pricing
Offer custom pricing to individual buyers:
$supplierService -> addPricingTier ( $catalogItem , [
'min_quantity' => 20 ,
'max_quantity' => null ,
'price' => 40.00
], $connection -> id ); // Specific buyer connection
Connection-specific pricing always takes priority over general tiers when calculating prices.
Price Calculation Logic
Check for connection-specific tiers
If a connection_id is provided, the system first searches for tiers matching that connection: $connectionTier = $tiersQuery
-> where ( 'connection_id' , $connectionId )
-> first ();
if ( $connectionTier ) {
return $connectionTier -> price ;
}
See: app/Services/SupplierService.php:201
Fall back to general tiers
If no connection-specific tier matches, use general tiers: $generalTier = $tiersQuery
-> whereNull ( 'connection_id' )
-> first ();
return $generalTier ? $generalTier -> price : $this -> base_wholesale_price ;
See: app/Models/SupplierCatalogItem.php:53
Use base price if no tiers match
If the quantity doesn’t match any tier range, the base_wholesale_price is used.
Retrieving Catalog Items
For Suppliers (Manage Catalog)
$catalogItems = SupplierCatalogItem :: forSupplier ( $tenantId )
-> with ([ 'product.variants' , 'pricingTiers' ])
-> latest ()
-> get ();
For Buyers (Browse Catalog)
$catalogItems = $supplierService -> getAvailableCatalog (
$supplierTenant , // Filter by supplier
$buyerTenant // Check visibility for this buyer
);
The getAvailableCatalog method:
Filters by is_available = true
Respects visibility rules (PUBLIC or CONNECTIONS_ONLY)
Eager loads product variants and pricing tiers
See: app/Services/SupplierService.php:149
Visibility filtering logic
The visibleTo scope uses a subquery to check connection status: public function scopeVisibleTo ( $query , $buyerTenantId )
{
return $query -> where ( function ( $q ) use ( $buyerTenantId ) {
$q -> where ( 'visibility' , CatalogVisibility :: PUBLIC )
-> orWhere ( function ( $subQ ) use ( $buyerTenantId ) {
$subQ -> where ( 'visibility' , CatalogVisibility :: CONNECTIONS_ONLY )
-> whereExists ( function ( $existsQuery ) use ( $buyerTenantId ) {
$existsQuery -> selectRaw ( '1' )
-> from ( 'supplier_connections' )
-> whereColumn ( 'supplier_connections.supplier_tenant_id' , 'supplier_catalog_items.supplier_tenant_id' )
-> where ( 'supplier_connections.buyer_tenant_id' , $buyerTenantId )
-> whereIn ( 'supplier_connections.status' , [
ConnectionStatus :: APPROVED -> value ,
ConnectionStatus :: ACTIVE -> value ,
]);
});
});
});
}
See: app/Models/SupplierCatalogItem.php:92
Updating Catalog Items
$supplierService -> updateCatalogItem ( $catalogItem , [
'is_available' => false ,
'base_wholesale_price' => 50.00 ,
'pricing_tiers' => [
[ 'min_quantity' => 10 , 'max_quantity' => null , 'price' => 48.00 ]
]
]);
Updating pricing_tiers deletes all existing general pricing tiers (where connection_id is null) and recreates them. Connection-specific tiers are preserved. See: app/Services/SupplierService.php:110
Minimum Order Quantities
Enforce minimum quantities per catalog item:
$catalogItem = SupplierCatalogItem :: create ([
'min_order_quantity' => 25 ,
// ...
]);
During purchase order creation, the system validates:
if ( $catalogItem -> min_order_quantity && $quantity < $catalogItem -> min_order_quantity ) {
throw new \Exception (
"Quantity { $quantity } is below the minimum order quantity of { $catalogItem -> min_order_quantity }"
);
}
See: app/Services/PurchaseOrderService.php:77
Pricing Tier Cache
The SupplierService caches pricing tier lookups to avoid N+1 queries:
// Preload tiers for multiple items
$supplierService -> preloadPricingTiers ( $catalogItemIds );
// Clear cache when tiers change
$supplierService -> clearTierCache ();
See: app/Services/SupplierService.php:226
Eager Loading
Always eager load relationships to prevent performance issues:
SupplierCatalogItem :: with ([
'product.variants' ,
'pricingTiers' ,
'supplierTenant'
]) -> get ();
Removing from Catalog
$supplierService -> removeFromCatalog ( $catalogItem );
Deleting a catalog item does NOT delete the underlying product. It only removes it from the supplier’s offering.
Integration with Purchase Orders
When creating a purchase order, buyers select catalog items:
$purchaseOrderService -> addItem ( $po , [
'catalog_item_id' => $catalogItem -> id ,
'quantity' => 50 ,
'product_variant_id' => $variant -> id ,
// unit_price is calculated automatically based on quantity and pricing tiers
]);
The service automatically:
Validates minimum order quantity
Calculates price using tiers and connection settings
Checks credit limits
See: app/Services/PurchaseOrderService.php:70