2025-12-22 17:42:59 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Feature\Payment;
|
|
|
|
|
|
|
|
|
|
use App\Models\Members\User;
|
|
|
|
|
use App\Models\Members\UserTenant;
|
|
|
|
|
use App\Models\Tenants\Payment;
|
|
|
|
|
use App\Models\Tenants\Plan;
|
|
|
|
|
use App\Models\Tenants\Subscription;
|
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
class PaymentApiTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use DatabaseTransactions;
|
|
|
|
|
|
|
|
|
|
private Tenant $tenant;
|
|
|
|
|
|
|
|
|
|
private User $user;
|
|
|
|
|
|
|
|
|
|
private Plan $plan;
|
|
|
|
|
|
|
|
|
|
private Subscription $subscription;
|
|
|
|
|
|
|
|
|
|
private string $apiKey;
|
|
|
|
|
|
|
|
|
|
private string $token;
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
parent::setUp();
|
|
|
|
|
|
|
|
|
|
// 테스트용 API Key 생성
|
|
|
|
|
$this->apiKey = 'test-api-key-'.uniqid();
|
|
|
|
|
\DB::table('api_keys')->insert([
|
|
|
|
|
'key' => $this->apiKey,
|
|
|
|
|
'description' => 'Test API Key',
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Tenant 생성 또는 기존 사용
|
|
|
|
|
$this->tenant = Tenant::first() ?? Tenant::withoutEvents(function () {
|
|
|
|
|
return Tenant::create([
|
|
|
|
|
'company_name' => 'Test Company',
|
|
|
|
|
'code' => 'TEST'.uniqid(),
|
|
|
|
|
'email' => 'test@example.com',
|
|
|
|
|
'phone' => '010-1234-5678',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Plan 생성
|
|
|
|
|
$this->plan = Plan::firstOrCreate(
|
|
|
|
|
['code' => 'TEST_BASIC'],
|
|
|
|
|
[
|
|
|
|
|
'name' => 'Basic Plan',
|
|
|
|
|
'description' => 'Test basic plan',
|
|
|
|
|
'price' => 10000,
|
|
|
|
|
'billing_cycle' => Plan::BILLING_MONTHLY,
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Subscription 생성
|
|
|
|
|
$this->subscription = Subscription::create([
|
|
|
|
|
'tenant_id' => $this->tenant->id,
|
|
|
|
|
'plan_id' => $this->plan->id,
|
|
|
|
|
'started_at' => now(),
|
|
|
|
|
'ended_at' => now()->addMonth(),
|
|
|
|
|
'status' => Subscription::STATUS_ACTIVE,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// User 생성
|
|
|
|
|
$testUserId = 'testuser'.uniqid();
|
|
|
|
|
$this->user = User::create([
|
|
|
|
|
'user_id' => $testUserId,
|
|
|
|
|
'name' => 'Test User',
|
|
|
|
|
'email' => $testUserId.'@example.com',
|
|
|
|
|
'password' => bcrypt('password123'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// UserTenant 관계 생성
|
|
|
|
|
UserTenant::create([
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'tenant_id' => $this->tenant->id,
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
'is_default' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 로그인 및 토큰 획득
|
|
|
|
|
$this->loginAndGetToken();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function loginAndGetToken(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->withHeaders([
|
|
|
|
|
'X-API-KEY' => $this->apiKey,
|
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
])->postJson('/api/v1/login', [
|
|
|
|
|
'user_id' => $this->user->user_id,
|
|
|
|
|
'user_pwd' => 'password123',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
$this->token = $response->json('access_token');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function authenticatedRequest(string $method, string $uri, array $data = [])
|
|
|
|
|
{
|
|
|
|
|
return $this->withHeaders([
|
|
|
|
|
'X-API-KEY' => $this->apiKey,
|
|
|
|
|
'Authorization' => 'Bearer '.$this->token,
|
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
])->{$method.'Json'}($uri, $data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== Payment List Tests ====================
|
|
|
|
|
|
|
|
|
|
public function test_can_list_payments(): void
|
|
|
|
|
{
|
|
|
|
|
// 결제 생성
|
|
|
|
|
Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('get', '/api/v1/payments');
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200)
|
|
|
|
|
->assertJsonStructure([
|
|
|
|
|
'success',
|
|
|
|
|
'message',
|
|
|
|
|
'data',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_can_get_payment_summary(): void
|
|
|
|
|
{
|
|
|
|
|
// 결제 생성
|
|
|
|
|
Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('get', '/api/v1/payments/summary');
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200)
|
|
|
|
|
->assertJsonStructure([
|
|
|
|
|
'success',
|
|
|
|
|
'message',
|
|
|
|
|
'data',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== Payment CRUD Tests ====================
|
|
|
|
|
|
|
|
|
|
public function test_can_create_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->authenticatedRequest('post', '/api/v1/payments', [
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 201 또는 200 (서비스 미구현 시 500)
|
|
|
|
|
$this->assertContains($response->status(), [200, 201, 500]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_can_show_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
2025-12-22 17:55:53 +09:00
|
|
|
'paid_at' => now(),
|
2025-12-22 17:42:59 +09:00
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('get', "/api/v1/payments/{$payment->id}");
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200)
|
|
|
|
|
->assertJsonStructure([
|
|
|
|
|
'success',
|
|
|
|
|
'message',
|
|
|
|
|
'data',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== Payment Action Tests ====================
|
|
|
|
|
|
|
|
|
|
public function test_can_complete_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
2025-12-22 17:55:53 +09:00
|
|
|
'paid_at' => now(),
|
2025-12-22 17:42:59 +09:00
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('post', "/api/v1/payments/{$payment->id}/complete", [
|
|
|
|
|
'transaction_id' => 'TXN_'.uniqid(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 200 (성공) 또는 서비스 미구현 시 500
|
|
|
|
|
$this->assertContains($response->status(), [200, 500]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_can_cancel_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
2025-12-22 17:55:53 +09:00
|
|
|
'paid_at' => now(),
|
2025-12-22 17:42:59 +09:00
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('post', "/api/v1/payments/{$payment->id}/cancel", [
|
|
|
|
|
'reason' => '테스트 취소',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 200 (성공) 또는 서비스 미구현 시 500
|
|
|
|
|
$this->assertContains($response->status(), [200, 500]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_can_refund_completed_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('post', "/api/v1/payments/{$payment->id}/refund", [
|
|
|
|
|
'reason' => '테스트 환불',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('payments', [
|
|
|
|
|
'id' => $payment->id,
|
|
|
|
|
'status' => Payment::STATUS_REFUNDED,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_can_get_payment_statement(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('get', "/api/v1/payments/{$payment->id}/statement");
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200)
|
|
|
|
|
->assertJsonStructure([
|
|
|
|
|
'success',
|
|
|
|
|
'message',
|
|
|
|
|
'data',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== Validation Tests ====================
|
|
|
|
|
|
|
|
|
|
public function test_cannot_create_payment_without_required_fields(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->authenticatedRequest('post', '/api/v1/payments', [
|
|
|
|
|
// subscription_id, amount 누락
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_cannot_refund_pending_payment(): void
|
|
|
|
|
{
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
'payment_method' => Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
2025-12-22 17:55:53 +09:00
|
|
|
'paid_at' => now(),
|
2025-12-22 17:42:59 +09:00
|
|
|
'created_by' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->authenticatedRequest('post', "/api/v1/payments/{$payment->id}/refund", [
|
|
|
|
|
'reason' => '환불 시도',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 400 또는 422 (대기 중인 결제는 환불 불가), 서비스 미구현 시 500
|
|
|
|
|
$this->assertContains($response->status(), [400, 422, 500]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== Authentication Tests ====================
|
|
|
|
|
|
|
|
|
|
public function test_cannot_access_payments_without_authentication(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->withHeaders([
|
|
|
|
|
'X-API-KEY' => $this->apiKey,
|
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
])->getJson('/api/v1/payments');
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_cannot_create_payment_without_authentication(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->withHeaders([
|
|
|
|
|
'X-API-KEY' => $this->apiKey,
|
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
])->postJson('/api/v1/payments', [
|
|
|
|
|
'subscription_id' => $this->subscription->id,
|
|
|
|
|
'amount' => 10000,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(401);
|
|
|
|
|
}
|
|
|
|
|
}
|