Services
EasyOTC uses service classes to encapsulate business logic and provide reusable functionality across the application. These services handle complex operations and maintain separation of concerns.
Where this lives
- Repo:
easy-otc-api(GitHub: Eliinova/the-one-otc-api) - Service classes:
app/Services/(e.g.CartService.php,CartItemService.php,FileUploadService.php) - Tests:
tests/Feature/Services/andtests/Unit/Services/
How to run
# From the easy-otc-api repo root
php artisan tinker # interactively call services
php artisan test # run the service test suiteServices are typically consumed from controllers or jobs running on staging (https://stage-api.easyotc.com) or locally (http://localhost:8000). See /access for the full URL list.
Available Services
1. CartService
The CartService manages shopping cart operations including creation, updates, and status management.
Location: app/Services/CartService.php
Key Features
- Cart Creation and Management: Create, find, and update carts
- Member Cart Operations: Get or create carts for specific members
- Status Management: Update cart status (active, completed, abandoned)
- Expiration Handling: Manage cart expiration dates
- Prescription Validation: Check prescription requirements
- Carrier Management: Update cart carrier associations
Methods
Cart Creation and Retrieval
// Create a new cart
$cart = $cartService->create([
'member_id' => $memberId,
'carrier_id' => $carrierId
]);
// Find cart by UUID
$cart = $cartService->findByUuid($uuid);
// Get or create cart for member
$cart = $cartService->getOrCreateForMember($memberId, $carrierId);
// Create or update cart with additional data
$cart = $cartService->createOrUpdateWithData($memberId, [
'carrier_id' => $carrierId,
'notes' => 'Special instructions'
]);Cart Status Management
// Update cart status
$cart = $cartService->updateStatus($cart, 'completed');Cart Operations
// Clear all items from cart
$cartService->clearCart($cart);
// Get cart total
$total = $cartService->getTotal($cart);
// Get cart item count
$itemCount = $cartService->getItemCount($cart);
// Check if cart has expired
$isExpired = $cartService->hasExpired($cart);
// Extend cart expiration
$cart = $cartService->extendExpiration($cart, 30); // 30 daysPrescription Management
// Check if cart requires prescription
$requiresPrescription = $cartService->requiresPrescription($cart);
// Get prescription status summary
$summary = $cartService->getPrescriptionStatusSummary($cart);
// Returns: ['total_prescription_items' => 2, 'pending' => 1, 'approved' => 1, 'rejected' => 0]Carrier Management
// Update carrier for cart
$cart = $cartService->updateCarrier($cart, $carrierId);
// Get active cart for member
$cart = $cartService->getActiveCartForMember($memberId);Usage Example
use App\Services\CartService;
class CartController extends Controller
{
public function __construct(private CartService $cartService) {}
public function addToCart(Request $request)
{
$memberId = $request->user()->member->id;
// Get or create cart for member
$cart = $this->cartService->getOrCreateForMember($memberId);
// Add items to cart using CartItemService
// ...
return response()->json([
'cart' => $cart,
'total' => $this->cartService->getTotal($cart),
'item_count' => $this->cartService->getItemCount($cart)
]);
}
}2. CartItemService
The CartItemService handles individual cart item operations including adding, updating, and removing items.
Location: app/Services/CartItemService.php
Key Features
- Item Management: Add, update, and remove cart items
- Quantity Management: Update item quantities with automatic price calculation
- Prescription Handling: Manage prescription status for items
- Product Snapshots: Create and manage product snapshots for cart items
- Batch Operations: Perform operations on multiple items
- Price Calculations: Handle unit price updates and discount calculations
Methods
Item Management
// Add item to cart
$cartItem = $cartItemService->addItem($cart, $product, 2, [
'notes' => 'Special instructions'
]);
// Update item quantity
$cartItem = $cartItemService->updateQuantity($cartItem, 3);
// Remove item from cart
$cartItemService->removeItem($cartItem);
// Set item quantity (replace existing)
$cartItem = $cartItemService->setQuantity($cartItem, 5);Item Updates
// Update item notes
$cartItem = $cartItemService->updateNotes($cartItem, 'New notes');
// Update prescription status
$cartItem = $cartItemService->updatePrescriptionStatus($cartItem, 'approved');
// Update unit price
$cartItem = $cartItemService->updateUnitPrice($cartItem, 15.99);Prescription Management
// Check if item requires prescription
$requiresPrescription = $cartItemService->requiresPrescription($cartItem);
// Get items by prescription status
$pendingItems = $cartItemService->getItemsByPrescriptionStatus($cart, 'pending');
// Get pending prescription items
$pendingItems = $cartItemService->getPendingPrescriptionItems($cart);
// Batch update prescription status
$updatedCount = $cartItemService->batchUpdatePrescriptionStatus([1, 2, 3], 'approved');Product Snapshots
// Get product snapshot
$snapshot = $cartItemService->getProductSnapshot($cartItem);
// Calculate item total with discount
$total = $cartItemService->calculateTotal($cartItem, 10); // 10% discountItem Retrieval
// Get cart item by product
$cartItem = $cartItemService->getCartItemByProduct($cart, $productId);Usage Example
use App\Services\CartItemService;
class CartItemController extends Controller
{
public function __construct(private CartItemService $cartItemService) {}
public function addToCart(Request $request)
{
$cart = $request->user()->member->activeCart;
$product = Product::findOrFail($request->product_id);
$quantity = $request->quantity ?? 1;
$cartItem = $this->cartItemService->addItem($cart, $product, $quantity, [
'notes' => $request->notes
]);
return response()->json([
'cart_item' => $cartItem,
'message' => 'Item added to cart successfully'
]);
}
public function updateQuantity(Request $request, CartItem $cartItem)
{
$quantity = $request->quantity;
if ($quantity <= 0) {
$this->cartItemService->removeItem($cartItem);
return response()->json(['message' => 'Item removed from cart']);
}
$cartItem = $this->cartItemService->updateQuantity($cartItem, $quantity);
return response()->json([
'cart_item' => $cartItem,
'message' => 'Quantity updated successfully'
]);
}
}3. FileUploadService
The FileUploadService handles file uploads to S3 storage with support for both regular file uploads and base64 data.
Location: app/Services/FileUploadService.php
Key Features
- S3 Integration: Upload files directly to Amazon S3
- Multiple Upload Types: Support for regular files and base64 data
- Unique Filename Generation: Automatic unique filename creation
- URL Generation: Generate public URLs for uploaded files
- File Deletion: Remove files from S3 storage
- Error Handling: Comprehensive error handling and logging
Methods
File Upload
// Store regular file upload
$url = $fileUploadService->store($request->file('image'), 'products');
// Store base64 image data
$url = $fileUploadService->storeFromBase64(
$base64Data,
'products',
'jpg',
'Product Name'
);File Management
// Delete file from S3
$deleted = $fileUploadService->delete($url);
// Generate S3 URL for a path
$url = $fileUploadService->generateUrl($path);Usage Example
use App\Services\FileUploadService;
class ProductController extends Controller
{
public function __construct(private FileUploadService $fileUploadService) {}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string',
'image' => 'nullable|image|max:2048',
'base64_image' => 'nullable|string'
]);
$productData = $request->only(['name', 'description', 'price']);
// Handle regular file upload
if ($request->hasFile('image')) {
$imageUrl = $this->fileUploadService->store(
$request->file('image'),
'products'
);
$productData['image_url'] = $imageUrl;
}
// Handle base64 image upload
if ($request->base64_image) {
$imageUrl = $this->fileUploadService->storeFromBase64(
$request->base64_image,
'products',
'jpg',
$request->name
);
$productData['image_url'] = $imageUrl;
}
$product = Product::create($productData);
return response()->json([
'product' => $product,
'message' => 'Product created successfully'
]);
}
public function update(Request $request, Product $product)
{
// Handle image update
if ($request->hasFile('image')) {
// Delete old image if exists
if ($product->image_url) {
$this->fileUploadService->delete($product->image_url);
}
// Upload new image
$imageUrl = $this->fileUploadService->store(
$request->file('image'),
'products'
);
$product->update(['image_url' => $imageUrl]);
}
return response()->json([
'product' => $product,
'message' => 'Product updated successfully'
]);
}
}Service Integration
Dependency Injection
Services can be injected into controllers and other classes using Laravel's dependency injection:
class CartController extends Controller
{
public function __construct(
private CartService $cartService,
private CartItemService $cartItemService
) {}
public function addToCart(Request $request)
{
$cart = $this->cartService->getOrCreateForMember($request->user()->member->id);
$product = Product::findOrFail($request->product_id);
$cartItem = $this->cartItemService->addItem($cart, $product, $request->quantity);
return response()->json([
'cart' => $this->cartService->getCartWithItems($cart),
'total' => $this->cartService->getTotal($cart)
]);
}
}Service Providers
Services can be registered in service providers for additional configuration:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(CartService::class, function ($app) {
return new CartService();
});
$this->app->singleton(CartItemService::class, function ($app) {
return new CartItemService();
});
$this->app->singleton(FileUploadService::class, function ($app) {
return new FileUploadService();
});
}Best Practices
1. Service Design
- Keep services focused on a single responsibility
- Use dependency injection for better testability
- Implement proper error handling and validation
- Document all public methods with clear descriptions
2. Cart Management
- Always validate cart ownership before operations
- Handle cart expiration gracefully
- Implement proper prescription validation workflows
- Use transactions for critical cart operations
3. File Uploads
- Validate file types and sizes before upload
- Implement proper error handling for upload failures
- Clean up old files when updating
- Use appropriate S3 permissions and visibility settings
4. Error Handling
- Implement comprehensive error handling in all services
- Log errors appropriately for debugging
- Return meaningful error messages to clients
- Handle edge cases gracefully
Testing Services
Services can be tested using Laravel's testing framework:
class CartServiceTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_cart_for_member()
{
$member = Member::factory()->create();
$cartService = new CartService();
$cart = $cartService->createOrUpdate($member->id);
$this->assertInstanceOf(Cart::class, $cart);
$this->assertEquals($member->id, $cart->member_id);
$this->assertEquals('active', $cart->status);
}
public function test_can_add_item_to_cart()
{
$cart = Cart::factory()->create();
$product = Product::factory()->create();
$cartItemService = new CartItemService();
$cartItem = $cartItemService->addItem($cart, $product, 2);
$this->assertInstanceOf(CartItem::class, $cartItem);
$this->assertEquals(2, $cartItem->quantity);
$this->assertEquals($product->price * 2, $cartItem->total_price);
}
}Configuration
S3 Configuration
The FileUploadService requires proper S3 configuration in config/filesystems.php:
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],Environment Variables
Required environment variables for file uploads:
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
AWS_URL=https://your-bucket.s3.amazonaws.com