User model
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
const TYPE_MERCHANT = 'merchant';
const TYPE_AFFILIATE = 'affiliate';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'type'
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function merchant(): HasOne
{
return $this->hasOne(Merchant::class);
}
public function affiliate(): HasOne
{
return $this->hasOne(Affiliate::class);
}
}
Merchant model
class Merchant extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'domain',
'display_name',
'turn_customers_into_affiliates',
'default_commission_rate'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
}
Affiliate model
class Affiliate extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'merchant_id',
'commission_rate',
'discount_code'
];
public function merchant()
{
return $this->belongsTo(Merchant::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function orders()
{
return $this->hasMany(Order::class);
}
}
Order model
class Order extends Model
{
use HasFactory;
const STATUS_UNPAID = 'unpaid';
const STATUS_PAID = 'paid';
protected $fillable = [
'external_order_id',
'merchant_id',
'affiliate_id',
'subtotal',
'commission_owed',
'payout_status',
'customer_email',
'created_at'
];
public function merchant()
{
return $this->belongsTo(Merchant::class);
}
public function affiliate()
{
return $this->belongsTo(Affiliate::class);
}
}
OrderServiceTest Case
class OrderServiceTest extends TestCase
{
use RefreshDatabase, WithFaker;
protected Merchant $merchant;
public function setUp(): void
{
parent::setUp();
$this->merchant = Merchant::factory()
->for(User::factory())
->create();
}
protected function getOrderService(): OrderService
{
return $this->app->make(OrderService::class);
}
public function test_create_order()
{
$data = [
'order_id' => $this->faker->uuid(),
'subtotal_price' => round(rand(100, 999) / 3, 2),
'merchant_domain' => $this->merchant->domain,
'discount_code' => $this->faker->uuid(),
'customer_email' => $this->faker->email(),
'customer_name' => $this->faker->name()
];
/** @var Affiliate $affiliate */
$affiliate = Affiliate::factory()
->for($this->merchant)
->for(User::factory())
->create([
'discount_code' => $data['discount_code']
]);
$this->mock(AffiliateService::class)
->shouldReceive('register')
->once()
->with(\Mockery::on(fn ($model) => $model->is($this->merchant)), $data['customer_email'], $data['customer_name'], 0.1);
$this->getOrderService()->processOrder($data);
$this->assertDatabaseHas('orders', [
'subtotal' => $data['subtotal_price'],
'affiliate_id' => $affiliate->id,
'merchant_id' => $this->merchant->id,
'commission_owed' => $data['subtotal_price'] * $affiliate->commission_rate,
'external_order_id' => $data['order_id']
]);
}
public function test_process_duplicate_order()
{
/** @var Order $order */
$order = Order::factory()
->for(Merchant::factory()->for(User::factory()))
->create();
$data = [
'order_id' => $order->external_order_id,
'subtotal_price' => round(rand(100, 999) / 3, 2),
'merchant_domain' => $this->merchant->domain,
'discount_code' => $this->faker->uuid(),
'customer_email' => $this->faker->email(),
'customer_name' => $this->faker->name()
];
$this->getOrderService()->processOrder($data);
$this->assertDatabaseCount('orders', 1);
}
}
This is the test case that I am trying to pass, as this test case is given by the client so I can not modify the test case at all below is the given solution which I have written
OrderService Class
class OrderService
{
public function __construct(
protected AffiliateService $affiliateService,
protected ApiService $apiService
) {}
/**
* Process an order and log any commissions.
* This should create a new affiliate if the customer_email is not already associated with one.
* This method should also ignore duplicates based on order_id.
*
* @param array{order_id: string, subtotal_price: float, merchant_domain: string, discount_code: string, customer_email: string, customer_name: string} $data
* @return void
*/
public function processOrder(array $data): void
{
if(!$this->getOrderByExternalOrderId($data['order_id'])){
$merchant = Merchant::where('domain', $data['merchant_domain'])->firstOrFail();
$affiliate = Affiliate::where('user_id', function ($query) use ($data) {
$query->select('id')
->from('users')
->where('email', $data['customer_email']);
})->first();
if (!$affiliate) {
$affiliate = $this->affiliateService->register($merchant, $data['customer_email'], $data['customer_name'], 0.1);
}
$order = new Order([
'external_order_id' => $data['order_id'],
'subtotal' => $data['subtotal_price'],
'affiliate_id' => $affiliate->id,
'merchant_id' => $merchant->id,
'commission_owed' => $data['subtotal_price'] * $affiliate->commission_rate,
]);
$order->save();
}
}
public function getOrderByExternalOrderId($externalOrderId){
return Order::where('external_order_id', $externalOrderId)->first();
}
}
Affilate Service
class AffiliateService
{
public function __construct(
protected ApiService $apiService,
protected Affiliate $affiliate
) {}
/**
* Create a new affiliate for the merchant with the given commission rate.
*
* @param Merchant $merchant
* @param string $email
* @param string $name
* @param float $commissionRate
* @return Affiliate
*/
public function register(Merchant $merchant, string $email, string $name, float $commissionRate): Affiliate
{
if ($this->getAffiliate($email)) {
throw new AffiliateCreateException('User already exists');
}
$affiliateUser = User::create([
'name' => $name,
'email' => $email,
'type' => User::TYPE_AFFILIATE,
]);
$discountCode = $this->apiService->createDiscountCode($merchant);
$affiliate = $this->affiliate->create([
'user_id' => $affiliateUser->id,
'merchant_id' => $merchant->id,
'commission_rate' => $commissionRate,
'discount_code' => $discountCode['code'],
]);
Mail::to($email)->send(new AffiliateCreated($affiliate));
return $affiliate;
}
public function getAffiliate($email): ?User
{
return User::where('email',$email)->first();
}
}
But I am getting the following error
Mockery\Exception\BadMethodCallException : Received Mockery_3_App_Models_Affiliate::getAttribute(), but no expectations were specified
/Users/mac/Downloads/testProject/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2222
/Users/mac/Downloads/testProject/app/Services/OrderService.php:43
/Users/mac/Downloads/testProject/tests/Feature/Services/OrderServiceTest.php:59