diff --git a/app/Http/Controllers/MerchantController.php b/app/Http/Controllers/MerchantController.php index ca12d1d..0ac2f56 100644 --- a/app/Http/Controllers/MerchantController.php +++ b/app/Http/Controllers/MerchantController.php @@ -6,13 +6,17 @@ use App\Services\MerchantService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use App\Models\Order; use Illuminate\Support\Carbon; class MerchantController extends Controller { + protected $merchantService; public function __construct( MerchantService $merchantService - ) {} + ) { + $this->merchantService = $merchantService; + } /** * Useful order statistics for the merchant API. @@ -22,6 +26,32 @@ public function __construct( */ public function orderStats(Request $request): JsonResponse { - // TODO: Complete this method + $from = $request->input('from'); + $to = $request->input('to'); + + // Get the authenticated user + $user = auth()->user(); + + // Assuming the user has a relationship with the merchant + $merchant = $user->merchant; + // Assuming the 'Order' model is imported at the top of the file + + $orders = Order::where('merchant_id', $merchant->id) + ->whereBetween('created_at', [$from, $to]) + ->get(); + + $count = $orders->count(); + $revenue = $orders->sum('subtotal'); + $commissionsOwed = $orders->filter(function ($order) { + return $order->affiliate_id !== null; + })->sum('commission_owed'); + + $response = [ + 'count' => $count, + 'revenue' => $revenue, + 'commissions_owed' => $commissionsOwed, + ]; + + return response()->json($response); } } diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index f2e69f1..d590f18 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -9,6 +9,7 @@ class WebhookController extends Controller { + public function __construct( protected OrderService $orderService ) {} @@ -21,6 +22,14 @@ public function __construct( */ public function __invoke(Request $request): JsonResponse { - // TODO: Complete this method + + $data = $request->all(); + + // Call the processOrder method of the OrderService + $this->orderService->processOrder($data); + + // You can customize the response based on the outcome of the processOrder method + return response()->json(['message' => 'Order processed successfully'], 200); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index c3be254..afe793e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -34,8 +34,9 @@ class Kernel extends HttpKernel \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\VerifyCsrfToken::class, +// \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], 'api' => [ diff --git a/app/Jobs/PayoutOrderJob.php b/app/Jobs/PayoutOrderJob.php index f3f062e..f7db487 100644 --- a/app/Jobs/PayoutOrderJob.php +++ b/app/Jobs/PayoutOrderJob.php @@ -11,6 +11,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\DB; +use RuntimeException; class PayoutOrderJob implements ShouldQueue { @@ -33,6 +34,18 @@ public function __construct( */ public function handle(ApiService $apiService) { - // TODO: Complete this method + try { + // Use the API service to send a payout + $apiService->sendPayout($this->order->affiliate->user->email, $this->order->commission_owed); + + // If the payout is successful, update the order status to paid + DB::transaction(function () { + $this->order->update(['payout_status' => Order::STATUS_PAID]); + }); + } catch (RuntimeException $exception) { + // If an exception is thrown during the payout, catch it + // and the order status will remain unpaid + throw $exception; + } } } diff --git a/app/Models/Merchant.php b/app/Models/Merchant.php index 9e5212f..331531e 100644 --- a/app/Models/Merchant.php +++ b/app/Models/Merchant.php @@ -20,6 +20,7 @@ class Merchant extends Model use HasFactory; protected $fillable = [ + 'user_id', 'domain', 'display_name', 'turn_customers_into_affiliates', diff --git a/app/Models/Order.php b/app/Models/Order.php index 8a196ba..521c6be 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -26,7 +26,8 @@ class Order extends Model 'subtotal', 'commission_owed', 'payout_status', - 'customer_email', + 'discount_code', + 'external_order_id', 'created_at' ]; diff --git a/app/Models/User.php b/app/Models/User.php index 5dbb8ed..8dce6d6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,7 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; use Laravel\Sanctum\HasApiTokens; +use Illuminate\Support\Str; /** * @property int $id @@ -67,4 +68,5 @@ public function affiliate(): HasOne { return $this->hasOne(Affiliate::class); } + } diff --git a/app/Services/AffiliateService.php b/app/Services/AffiliateService.php index a45da18..ab024c8 100644 --- a/app/Services/AffiliateService.php +++ b/app/Services/AffiliateService.php @@ -9,7 +9,7 @@ use App\Models\Order; use App\Models\User; use Illuminate\Support\Facades\Mail; - +use Illuminate\Support\Str; class AffiliateService { public function __construct( @@ -27,6 +27,40 @@ public function __construct( */ public function register(Merchant $merchant, string $email, string $name, float $commissionRate): Affiliate { - // TODO: Complete this method + // Check if the email is already in use by the merchant's user + if ($merchant->user->email === $email) { + throw new AffiliateCreateException('Email is already in use by the merchant'); + } + + // Check if the email is already in use by another affiliate for the same merchant + if (Affiliate::where('merchant_id', $merchant->id)->whereHas('user', function ($query) use ($email) { + $query->where('email', $email); + })->exists()) { + throw new AffiliateCreateException('Email is already in use by another affiliate for the same merchant'); + } + + // Create a new user for the affiliate + $user = User::create([ + 'name' => $name, + 'email' => $email, + 'password' => bcrypt(Str::random(16)), + 'type' => User::TYPE_MERCHANT, + ]); + + // Create a new discount code for the affiliate + $discountCode = $this->apiService->createDiscountCode($merchant); + + // Create a new affiliate for the merchant + $affiliate = Affiliate::create([ + 'user_id' => $user->id, + 'merchant_id'=>$merchant->id, + 'commission_rate' => $commissionRate, + 'discount_code' => $discountCode['code'], + ]); + + // Send an email to the affiliate + Mail::to($affiliate->user)->send(new AffiliateCreated($affiliate)); + + return $affiliate; } } diff --git a/app/Services/MerchantService.php b/app/Services/MerchantService.php index 2fd27b9..ff9cf42 100644 --- a/app/Services/MerchantService.php +++ b/app/Services/MerchantService.php @@ -7,6 +7,7 @@ use App\Models\Merchant; use App\Models\Order; use App\Models\User; +use Illuminate\Support\Facades\Hash; class MerchantService { @@ -20,7 +21,22 @@ class MerchantService */ public function register(array $data): Merchant { - // TODO: Complete this method + + $user = User::create([ + 'email' => $data['email'], + 'name' => $data['name'], + 'password' => $data['api_key'], + 'type' => User::TYPE_MERCHANT, + ]); + + + $merchant = Merchant::create([ + 'domain' => $data['domain'], + 'display_name' => $data['name'], + 'user_id' => $user->id, + ]); + + return $merchant; } /** @@ -31,7 +47,15 @@ public function register(array $data): Merchant */ public function updateMerchant(User $user, array $data) { - // TODO: Complete this method + $user->update([ + 'email' => $data['email'], + 'password' => Hash::make($data['api_key']), + ]); + + $user->merchant->update([ + 'domain' => $data['domain'], + 'display_name' => $data['name'], + ]); } /** @@ -43,7 +67,9 @@ public function updateMerchant(User $user, array $data) */ public function findMerchantByEmail(string $email): ?Merchant { - // TODO: Complete this method + $user = User::where('email', $email)->first(); + + return $user ? $user->merchant : null; } /** @@ -55,6 +81,10 @@ public function findMerchantByEmail(string $email): ?Merchant */ public function payout(Affiliate $affiliate) { - // TODO: Complete this method + $unpaidOrders = $affiliate->orders()->where('payout_status', Order::STATUS_UNPAID)->get(); + + foreach ($unpaidOrders as $order) { + PayoutOrderJob::dispatch($order); + } } } diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index 842ddb9..7d1c4a3 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -23,6 +23,36 @@ public function __construct( */ public function processOrder(array $data) { - // TODO: Complete this method + $order = Order::where('external_order_id', $data['order_id'])->first(); + + // Check if the order with the given order_id already exists + if ($order) { + return; // Ignore duplicate orders + } + + $affiliate = Affiliate::where('discount_code', $data['discount_code'])->first(); + + // If no affiliate found, create a new one + if (!$affiliate) { + $affiliate = Affiliate::create([ + 'merchant_id' => Merchant::where('domain', $data['merchant_domain'])->value('id'), + 'user_id' => User::where('email', $data['customer_email'])->value('id'), + 'discount_code' => $data['discount_code'], + 'commission_rate' => 0.1, + ]); + } + + // Register the affiliate + $this->affiliateService->register($affiliate->merchant, $data['customer_email'], $data['customer_name'], 0.1); + + // Create the order + Order::create([ + 'subtotal' => $data['subtotal_price'], + 'affiliate_id' => $affiliate->id, + 'merchant_id' => $affiliate->merchant_id, + 'commission_owed' => $data['subtotal_price'] * $affiliate->commission_rate, + 'external_order_id' => $data['order_id'], + 'payout_status' => Order::STATUS_UNPAID, + ]); } } diff --git a/database/factories/AffiliateFactory.php b/database/factories/AffiliateFactory.php index c66c1f1..7c47f82 100644 --- a/database/factories/AffiliateFactory.php +++ b/database/factories/AffiliateFactory.php @@ -17,6 +17,8 @@ class AffiliateFactory extends Factory public function definition() { return [ + 'user_id'=>$this->faker->numberBetween(1,10), + 'merchant_id'=>$this->faker->numberBetween(1,10), 'discount_code' => $this->faker->uuid(), 'commission_rate' => round(rand(1, 5) / 10, 1) ]; diff --git a/database/factories/MerchantFactory.php b/database/factories/MerchantFactory.php index 846df6c..20cc02d 100644 --- a/database/factories/MerchantFactory.php +++ b/database/factories/MerchantFactory.php @@ -17,8 +17,9 @@ class MerchantFactory extends Factory public function definition() { return [ - 'domain' => $this->faker->domainName(), - 'display_name' => $this->faker->name() + 'user_id' => $this->faker->numberBetween(1,10), + 'domain' => $this->faker->unique()->domainName(), + 'display_name' => $this->faker->name(), ]; } } diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php index f4b0c3f..511fe8b 100644 --- a/database/factories/OrderFactory.php +++ b/database/factories/OrderFactory.php @@ -17,6 +17,7 @@ class OrderFactory extends Factory public function definition() { return [ + 'merchant_id' =>$this->faker->numberBetween(1,10), 'subtotal' => $subtotal = round(rand(100, 999) / 3, 2), 'commission_owed' => round($subtotal * 0.1, 2), ]; diff --git a/database/migrations/2022_05_13_220658_create_affiliates_table.php b/database/migrations/2022_05_13_220658_create_affiliates_table.php index 93b49de..c6dfe9b 100644 --- a/database/migrations/2022_05_13_220658_create_affiliates_table.php +++ b/database/migrations/2022_05_13_220658_create_affiliates_table.php @@ -18,7 +18,8 @@ public function up() $table->foreignId('user_id'); $table->foreignId('merchant_id'); // TODO: Replace me with a brief explanation of why floats aren't the correct data type, and replace with the correct data type. - $table->float('commission_rate'); + // ANS : Floats are not the correct data type for representing monetary values because they can result in rounding errors and imprecise calculations. The correct data type for representing monetary values is decimal. + $table->decimal('commission_rate', 8, 2); $table->string('discount_code'); $table->timestamps(); }); diff --git a/database/migrations/2022_05_16_143445_create_orders_table.php b/database/migrations/2022_05_16_143445_create_orders_table.php index fd5a10b..73c0d9d 100644 --- a/database/migrations/2022_05_16_143445_create_orders_table.php +++ b/database/migrations/2022_05_16_143445_create_orders_table.php @@ -19,10 +19,12 @@ public function up() $table->foreignId('merchant_id')->constrained(); $table->foreignId('affiliate_id')->nullable()->constrained(); // TODO: Replace floats with the correct data types (very similar to affiliates table) - $table->float('subtotal'); - $table->float('commission_owed')->default(0.00); + // ANS : Floats are not the correct data type for representing monetary values because they can result in rounding errors and imprecise calculations. The correct data type for representing monetary values is decimal. + $table->decimal('subtotal',10,2); + $table->decimal('commission_owed',10,2)->default(0.00); $table->string('payout_status')->default(Order::STATUS_UNPAID); $table->string('discount_code')->nullable(); + $table->string('external_order_id')->nullable(); $table->timestamps(); }); }