CustomerInvoiceTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <?php
  2. namespace Webkul\BagistoApi\Tests\Feature\GraphQL;
  3. use Webkul\BagistoApi\Tests\GraphQLTestCase;
  4. use Webkul\Core\Models\Channel;
  5. use Webkul\Customer\Models\Customer;
  6. use Webkul\Product\Models\Product;
  7. use Webkul\Sales\Models\Invoice;
  8. use Webkul\Sales\Models\InvoiceItem;
  9. use Webkul\Sales\Models\Order;
  10. use Webkul\Sales\Models\OrderItem;
  11. use Webkul\Sales\Models\OrderPayment;
  12. class CustomerInvoiceTest extends GraphQLTestCase
  13. {
  14. /**
  15. * Create test data — customer with order and invoices
  16. */
  17. private function createTestData(): array
  18. {
  19. $this->seedRequiredData();
  20. $customer = $this->createCustomer();
  21. $channel = Channel::first();
  22. $product = Product::factory()->create();
  23. $order = Order::factory()->create([
  24. 'customer_id' => $customer->id,
  25. 'customer_email' => $customer->email,
  26. 'customer_first_name' => $customer->first_name,
  27. 'customer_last_name' => $customer->last_name,
  28. 'channel_id' => $channel->id,
  29. 'status' => 'completed',
  30. ]);
  31. $orderItem = OrderItem::factory()->create([
  32. 'order_id' => $order->id,
  33. 'product_id' => $product->id,
  34. 'sku' => 'TEST-INV-SKU-001',
  35. 'type' => 'simple',
  36. 'name' => 'Test Invoice Product',
  37. ]);
  38. OrderPayment::factory()->create([
  39. 'order_id' => $order->id,
  40. ]);
  41. /** Create first invoice (paid) */
  42. $invoice1 = Invoice::factory()->create([
  43. 'order_id' => $order->id,
  44. 'state' => 'paid',
  45. 'total_qty' => 2,
  46. 'sub_total' => 100.00,
  47. 'base_sub_total' => 100.00,
  48. 'grand_total' => 110.00,
  49. 'base_grand_total' => 110.00,
  50. 'shipping_amount' => 5.00,
  51. 'base_shipping_amount' => 5.00,
  52. 'tax_amount' => 5.00,
  53. 'base_tax_amount' => 5.00,
  54. 'discount_amount' => 0.00,
  55. 'base_discount_amount' => 0.00,
  56. 'base_currency_code' => 'USD',
  57. 'order_currency_code' => 'USD',
  58. 'increment_id' => 'INV-001',
  59. ]);
  60. InvoiceItem::factory()->create([
  61. 'invoice_id' => $invoice1->id,
  62. 'order_item_id' => $orderItem->id,
  63. 'name' => 'Test Invoice Product',
  64. 'sku' => 'TEST-INV-SKU-001',
  65. 'qty' => 2,
  66. 'price' => 50.00,
  67. 'base_price' => 50.00,
  68. 'total' => 100.00,
  69. 'base_total' => 100.00,
  70. ]);
  71. /** Create second invoice (pending) */
  72. $invoice2 = Invoice::factory()->create([
  73. 'order_id' => $order->id,
  74. 'state' => 'pending',
  75. 'total_qty' => 1,
  76. 'sub_total' => 50.00,
  77. 'base_sub_total' => 50.00,
  78. 'grand_total' => 55.00,
  79. 'base_grand_total' => 55.00,
  80. 'shipping_amount' => 3.00,
  81. 'base_shipping_amount' => 3.00,
  82. 'tax_amount' => 2.00,
  83. 'base_tax_amount' => 2.00,
  84. 'discount_amount' => 0.00,
  85. 'base_discount_amount' => 0.00,
  86. 'base_currency_code' => 'USD',
  87. 'order_currency_code' => 'USD',
  88. 'increment_id' => 'INV-002',
  89. ]);
  90. InvoiceItem::factory()->create([
  91. 'invoice_id' => $invoice2->id,
  92. 'order_item_id' => $orderItem->id,
  93. 'name' => 'Test Invoice Product',
  94. 'sku' => 'TEST-INV-SKU-001',
  95. 'qty' => 1,
  96. 'price' => 50.00,
  97. 'base_price' => 50.00,
  98. 'total' => 50.00,
  99. 'base_total' => 50.00,
  100. ]);
  101. return compact('customer', 'channel', 'product', 'order', 'orderItem', 'invoice1', 'invoice2');
  102. }
  103. // ── Collection Queries ────────────────────────────────────
  104. /**
  105. * Test: Query all customer invoices collection
  106. */
  107. public function test_get_customer_invoices_collection(): void
  108. {
  109. $testData = $this->createTestData();
  110. $query = <<<'GQL'
  111. query getCustomerInvoices {
  112. customerInvoices(first: 10) {
  113. edges {
  114. cursor
  115. node {
  116. _id
  117. incrementId
  118. state
  119. totalQty
  120. grandTotal
  121. baseGrandTotal
  122. subTotal
  123. baseSubTotal
  124. shippingAmount
  125. taxAmount
  126. discountAmount
  127. baseCurrencyCode
  128. orderCurrencyCode
  129. createdAt
  130. }
  131. }
  132. pageInfo {
  133. endCursor
  134. startCursor
  135. hasNextPage
  136. hasPreviousPage
  137. }
  138. totalCount
  139. }
  140. }
  141. GQL;
  142. $response = $this->authenticatedGraphQL($testData['customer'], $query);
  143. $response->assertOk();
  144. $data = $response->json('data.customerInvoices');
  145. expect($data['totalCount'])->toBeGreaterThanOrEqual(2);
  146. expect($data['edges'])->not()->toBeEmpty();
  147. }
  148. /**
  149. * Test: Unauthenticated request returns error
  150. */
  151. public function test_get_customer_invoices_requires_authentication(): void
  152. {
  153. $query = <<<'GQL'
  154. query getCustomerInvoices {
  155. customerInvoices(first: 5) {
  156. edges {
  157. node {
  158. _id
  159. }
  160. }
  161. }
  162. }
  163. GQL;
  164. $response = $this->graphQL($query);
  165. $response->assertOk();
  166. $errors = $response->json('errors');
  167. expect($errors)->not()->toBeEmpty();
  168. }
  169. /**
  170. * Test: Customer only sees invoices from their own orders
  171. */
  172. public function test_customer_only_sees_own_invoices(): void
  173. {
  174. $testData = $this->createTestData();
  175. /** Create another customer with their own order and invoice */
  176. $otherCustomer = $this->createCustomer();
  177. $channel = Channel::first();
  178. $otherOrder = Order::factory()->create([
  179. 'customer_id' => $otherCustomer->id,
  180. 'customer_email' => $otherCustomer->email,
  181. 'customer_first_name' => $otherCustomer->first_name,
  182. 'customer_last_name' => $otherCustomer->last_name,
  183. 'channel_id' => $channel->id,
  184. 'status' => 'completed',
  185. ]);
  186. Invoice::factory()->create([
  187. 'order_id' => $otherOrder->id,
  188. 'state' => 'paid',
  189. 'grand_total' => 200.00,
  190. 'base_grand_total' => 200.00,
  191. ]);
  192. $query = <<<'GQL'
  193. query getCustomerInvoices {
  194. customerInvoices(first: 50) {
  195. edges {
  196. node {
  197. _id
  198. }
  199. }
  200. totalCount
  201. }
  202. }
  203. GQL;
  204. $response = $this->authenticatedGraphQL($testData['customer'], $query);
  205. $response->assertOk();
  206. $data = $response->json('data.customerInvoices');
  207. /** Should only see the 2 invoices belonging to testData customer */
  208. expect($data['totalCount'])->toBe(2);
  209. }
  210. /**
  211. * Test: Filter invoices by orderId
  212. */
  213. public function test_filter_invoices_by_order_id(): void
  214. {
  215. $testData = $this->createTestData();
  216. $query = <<<'GQL'
  217. query getCustomerInvoices($orderId: Int) {
  218. customerInvoices(first: 10, orderId: $orderId) {
  219. edges {
  220. node {
  221. _id
  222. }
  223. }
  224. totalCount
  225. }
  226. }
  227. GQL;
  228. $response = $this->authenticatedGraphQL($testData['customer'], $query, [
  229. 'orderId' => $testData['order']->id,
  230. ]);
  231. $response->assertOk();
  232. $data = $response->json('data.customerInvoices');
  233. expect($data['totalCount'])->toBe(2);
  234. }
  235. /**
  236. * Test: Filter invoices by state
  237. */
  238. public function test_filter_invoices_by_state(): void
  239. {
  240. $testData = $this->createTestData();
  241. $query = <<<'GQL'
  242. query getCustomerInvoices($state: String) {
  243. customerInvoices(first: 10, state: $state) {
  244. edges {
  245. node {
  246. _id
  247. state
  248. }
  249. }
  250. totalCount
  251. }
  252. }
  253. GQL;
  254. $response = $this->authenticatedGraphQL($testData['customer'], $query, [
  255. 'state' => 'paid',
  256. ]);
  257. $response->assertOk();
  258. $data = $response->json('data.customerInvoices');
  259. expect($data['totalCount'])->toBe(1);
  260. $node = $data['edges'][0]['node'];
  261. expect($node['state'])->toBe('paid');
  262. }
  263. /**
  264. * Test: Filter by pending state
  265. */
  266. public function test_filter_invoices_by_pending_state(): void
  267. {
  268. $testData = $this->createTestData();
  269. $query = <<<'GQL'
  270. query getCustomerInvoices($state: String) {
  271. customerInvoices(first: 10, state: $state) {
  272. edges {
  273. node {
  274. _id
  275. state
  276. }
  277. }
  278. totalCount
  279. }
  280. }
  281. GQL;
  282. $response = $this->authenticatedGraphQL($testData['customer'], $query, [
  283. 'state' => 'pending',
  284. ]);
  285. $response->assertOk();
  286. $data = $response->json('data.customerInvoices');
  287. expect($data['totalCount'])->toBe(1);
  288. $node = $data['edges'][0]['node'];
  289. expect($node['state'])->toBe('pending');
  290. }
  291. // ── Single Item Query ─────────────────────────────────────
  292. /**
  293. * Test: Query single customer invoice by ID
  294. */
  295. public function test_get_customer_invoice_by_id(): void
  296. {
  297. $testData = $this->createTestData();
  298. $invoiceId = "/api/shop/customer-invoices/{$testData['invoice1']->id}";
  299. $query = <<<GQL
  300. query getCustomerInvoice {
  301. customerInvoice(id: "{$invoiceId}") {
  302. _id
  303. incrementId
  304. state
  305. totalQty
  306. grandTotal
  307. baseGrandTotal
  308. subTotal
  309. baseSubTotal
  310. shippingAmount
  311. baseShippingAmount
  312. taxAmount
  313. baseTaxAmount
  314. discountAmount
  315. baseDiscountAmount
  316. baseCurrencyCode
  317. orderCurrencyCode
  318. createdAt
  319. }
  320. }
  321. GQL;
  322. $response = $this->graphQL($query);
  323. $response->assertOk();
  324. $data = $response->json('data.customerInvoice');
  325. expect($data['_id'])->toBe($testData['invoice1']->id);
  326. expect($data['state'])->toBe('paid');
  327. expect($data['incrementId'])->toBe('INV-001');
  328. }
  329. /**
  330. * Test: Query invoice returns correct financial data
  331. */
  332. public function test_invoice_returns_financial_data(): void
  333. {
  334. $testData = $this->createTestData();
  335. $invoiceId = "/api/shop/customer-invoices/{$testData['invoice1']->id}";
  336. $query = <<<GQL
  337. query getCustomerInvoice {
  338. customerInvoice(id: "{$invoiceId}") {
  339. _id
  340. grandTotal
  341. baseGrandTotal
  342. subTotal
  343. baseSubTotal
  344. taxAmount
  345. shippingAmount
  346. discountAmount
  347. }
  348. }
  349. GQL;
  350. $response = $this->graphQL($query);
  351. $response->assertOk();
  352. $data = $response->json('data.customerInvoice');
  353. expect($data)->toHaveKeys([
  354. '_id',
  355. 'grandTotal',
  356. 'baseGrandTotal',
  357. 'subTotal',
  358. 'baseSubTotal',
  359. 'taxAmount',
  360. 'shippingAmount',
  361. 'discountAmount',
  362. ]);
  363. }
  364. /**
  365. * Test: Query non-existent invoice returns error
  366. */
  367. public function test_get_nonexistent_invoice_returns_error(): void
  368. {
  369. $this->seedRequiredData();
  370. $query = <<<'GQL'
  371. query getCustomerInvoice {
  372. customerInvoice(id: "/api/shop/customer-invoices/99999") {
  373. _id
  374. state
  375. }
  376. }
  377. GQL;
  378. $response = $this->graphQL($query);
  379. $response->assertOk();
  380. $errors = $response->json('errors');
  381. expect($errors)->not()->toBeEmpty();
  382. }
  383. // ── Pagination ────────────────────────────────────────────
  384. /**
  385. * Test: Pagination with first parameter
  386. */
  387. public function test_pagination_with_first_parameter(): void
  388. {
  389. $testData = $this->createTestData();
  390. $query = <<<'GQL'
  391. query getCustomerInvoices {
  392. customerInvoices(first: 1) {
  393. edges {
  394. cursor
  395. node {
  396. _id
  397. }
  398. }
  399. pageInfo {
  400. endCursor
  401. hasNextPage
  402. }
  403. totalCount
  404. }
  405. }
  406. GQL;
  407. $response = $this->authenticatedGraphQL($testData['customer'], $query);
  408. $response->assertOk();
  409. $data = $response->json('data.customerInvoices');
  410. expect(count($data['edges']))->toBe(1);
  411. expect($data['totalCount'])->toBeGreaterThanOrEqual(2);
  412. }
  413. /**
  414. * Test: Forward pagination with after cursor
  415. */
  416. public function test_pagination_with_after_cursor(): void
  417. {
  418. $testData = $this->createTestData();
  419. /** First page */
  420. $query = <<<'GQL'
  421. query getCustomerInvoices {
  422. customerInvoices(first: 1) {
  423. edges {
  424. cursor
  425. node {
  426. _id
  427. }
  428. }
  429. pageInfo {
  430. endCursor
  431. hasNextPage
  432. }
  433. }
  434. }
  435. GQL;
  436. $response = $this->authenticatedGraphQL($testData['customer'], $query);
  437. $response->assertOk();
  438. $firstPageData = $response->json('data.customerInvoices');
  439. $endCursor = $firstPageData['pageInfo']['endCursor'];
  440. $firstInvoiceId = $firstPageData['edges'][0]['node']['_id'];
  441. /** Second page */
  442. $query2 = <<<GQL
  443. query getCustomerInvoices {
  444. customerInvoices(first: 1, after: "{$endCursor}") {
  445. edges {
  446. node {
  447. _id
  448. }
  449. }
  450. }
  451. }
  452. GQL;
  453. $response2 = $this->authenticatedGraphQL($testData['customer'], $query2);
  454. $response2->assertOk();
  455. $secondPageData = $response2->json('data.customerInvoices');
  456. $secondInvoiceId = $secondPageData['edges'][0]['node']['_id'] ?? null;
  457. /** Second page should have a different invoice */
  458. expect($secondInvoiceId)->not()->toBe($firstInvoiceId);
  459. }
  460. // ── Schema Introspection ──────────────────────────────────
  461. /**
  462. * Test: CustomerInvoice type has expected fields in schema
  463. */
  464. public function test_customer_invoice_schema_has_expected_fields(): void
  465. {
  466. $query = <<<'GQL'
  467. {
  468. __type(name: "CustomerInvoice") {
  469. name
  470. fields {
  471. name
  472. }
  473. }
  474. }
  475. GQL;
  476. $response = $this->graphQL($query);
  477. $response->assertSuccessful();
  478. $type = $response->json('data.__type');
  479. expect($type)->not->toBeNull()
  480. ->and($type['name'])->toBe('CustomerInvoice');
  481. $fieldNames = array_column($type['fields'], 'name');
  482. expect($fieldNames)
  483. ->toContain('_id')
  484. ->toContain('incrementId')
  485. ->toContain('state')
  486. ->toContain('totalQty')
  487. ->toContain('grandTotal')
  488. ->toContain('subTotal')
  489. ->toContain('shippingAmount')
  490. ->toContain('taxAmount')
  491. ->toContain('discountAmount')
  492. ->toContain('baseCurrencyCode')
  493. ->toContain('orderCurrencyCode')
  494. ->toContain('createdAt');
  495. }
  496. }