Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assessment-task #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions app/Http/Controllers/MerchantController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
}
11 changes: 10 additions & 1 deletion app/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class WebhookController extends Controller
{

public function __construct(
protected OrderService $orderService
) {}
Expand All @@ -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);

}
}
3 changes: 2 additions & 1 deletion app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
Expand Down
15 changes: 14 additions & 1 deletion app/Jobs/PayoutOrderJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class PayoutOrderJob implements ShouldQueue
{
Expand All @@ -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;
}
}
}
1 change: 1 addition & 0 deletions app/Models/Merchant.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Merchant extends Model
use HasFactory;

protected $fillable = [
'user_id',
'domain',
'display_name',
'turn_customers_into_affiliates',
Expand Down
3 changes: 2 additions & 1 deletion app/Models/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class Order extends Model
'subtotal',
'commission_owed',
'payout_status',
'customer_email',
'discount_code',
'external_order_id',
'created_at'
];

Expand Down
2 changes: 2 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Support\Str;

/**
* @property int $id
Expand Down Expand Up @@ -67,4 +68,5 @@ public function affiliate(): HasOne
{
return $this->hasOne(Affiliate::class);
}

}
38 changes: 36 additions & 2 deletions app/Services/AffiliateService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}
}
38 changes: 34 additions & 4 deletions app/Services/MerchantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Merchant;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class MerchantService
{
Expand All @@ -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;
}

/**
Expand All @@ -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'],
]);
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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);
}
}
}
32 changes: 31 additions & 1 deletion app/Services/OrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
}
}
2 changes: 2 additions & 0 deletions database/factories/AffiliateFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
];
Expand Down
5 changes: 3 additions & 2 deletions database/factories/MerchantFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}
}
1 change: 1 addition & 0 deletions database/factories/OrderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
6 changes: 4 additions & 2 deletions database/migrations/2022_05_16_143445_create_orders_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down