/** * Subscription integration tests * * NOTE: These tests require the subscription feature to be enabled in the iyzico merchant panel. * If the feature is not enabled, tests will be skipped automatically. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { createTestClient, assertSuccessResponse } from '../setup'; import { testProductCreateRequest, testPricingPlanCreateRequest, testSubscriptionCustomer, testSubscriptionPaymentCard, generateTestProductName, generateTestPlanName, generateTestEmail, } from '../fixtures/subscription'; import { IyzicoResponseError } from '../../src/errors'; describe('Subscription Integration Tests', () => { const client = createTestClient(); // Flag to track if subscription feature is available let subscriptionFeatureAvailable = true; // Shared references for chained tests let createdProductReferenceCode: string | undefined; let createdPricingPlanReferenceCode: string | undefined; let createdSubscriptionReferenceCode: string | undefined; let createdCustomerReferenceCode: string | undefined; // Helper to skip test if subscription feature is not available const skipIfUnavailable = () => { if (!subscriptionFeatureAvailable) { console.log('Skipping: Subscription feature not enabled in merchant panel'); return true; } return false; }; // ============================================================================ // Product CRUD Tests // ============================================================================ describe('Product Operations', () => { it('should create a subscription product', async () => { const request = { ...testProductCreateRequest, // conversationId: `test-product-create-${Date.now()}`, // name: generateTestProductName(), }; try { const response = await client.subscription.createProduct(request); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBeTruthy(); expect(response.data?.name).toBe(request.name); expect(response.data?.status).toBe('ACTIVE'); // Save for later tests createdProductReferenceCode = response.data?.referenceCode; } catch (error) { // Check if subscription feature is not enabled (error code 100001 = system error) if (error instanceof IyzicoResponseError && error.errorResponse?.errorCode === '100001') { subscriptionFeatureAvailable = false; console.log('Subscription feature not enabled in merchant panel. Skipping subscription tests.'); return; // Skip this test gracefully } throw error; } }); it('should get a subscription product', async () => { if (skipIfUnavailable() || !createdProductReferenceCode) { return; // Skip if feature unavailable or no product created } const response = await client.subscription.getProduct(createdProductReferenceCode); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBe(createdProductReferenceCode); }); it('should update a subscription product', async () => { if (skipIfUnavailable() || !createdProductReferenceCode) { return; // Skip if feature unavailable or no product created } const updatedName = `Updated Product ${Date.now()}`; const response = await client.subscription.updateProduct(createdProductReferenceCode, { name: updatedName, description: 'Updated description', conversationId: `test-product-update-${Date.now()}`, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.name).toBe(updatedName); }); it('should list subscription products', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } const response = await client.subscription.listProducts({ page: 1, count: 10, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.items).toBeInstanceOf(Array); expect(response.data?.totalCount).toBeGreaterThanOrEqual(0); }); }); // ============================================================================ // Pricing Plan CRUD Tests // ============================================================================ describe('Pricing Plan Operations', () => { it('should create a pricing plan', async () => { if (skipIfUnavailable() || !createdProductReferenceCode) { return; // Skip if feature unavailable or no product created } const request = { ...testPricingPlanCreateRequest, conversationId: `test-plan-create-${Date.now()}`, name: generateTestPlanName(), }; const response = await client.subscription.createPricingPlan(createdProductReferenceCode, request); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBeTruthy(); expect(response.data?.name).toBe(request.name); expect(response.data?.price).toBe(request.price); expect(response.data?.paymentInterval).toBe(request.paymentInterval); // Save for later tests createdPricingPlanReferenceCode = response.data?.referenceCode; }); it('should get a pricing plan', async () => { if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { return; // Skip if feature unavailable or no plan created } const response = await client.subscription.getPricingPlan(createdPricingPlanReferenceCode); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBe(createdPricingPlanReferenceCode); }); it('should update a pricing plan', async () => { if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { return; // Skip if feature unavailable or no plan created } const updatedName = `Updated Plan ${Date.now()}`; const response = await client.subscription.updatePricingPlan(createdPricingPlanReferenceCode, { name: updatedName, trialPeriodDays: 14, conversationId: `test-plan-update-${Date.now()}`, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.name).toBe(updatedName); }); it('should list pricing plans for a product', async () => { if (skipIfUnavailable() || !createdProductReferenceCode) { return; // Skip if feature unavailable or no product created } const response = await client.subscription.listPricingPlans(createdProductReferenceCode, { page: 1, count: 10, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.items).toBeInstanceOf(Array); }); }); // ============================================================================ // Subscription Lifecycle Tests // ============================================================================ describe('Subscription Operations', () => { it('should initialize a subscription', async () => { if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { return; // Skip if feature unavailable or no plan created } const uniqueEmail = generateTestEmail(); const response = await client.subscription.initialize({ locale: 'tr', conversationId: `test-sub-init-${Date.now()}`, pricingPlanReferenceCode: createdPricingPlanReferenceCode, subscriptionInitialStatus: 'ACTIVE', customer: { ...testSubscriptionCustomer, email: uniqueEmail, }, paymentCard: testSubscriptionPaymentCard, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBeTruthy(); expect(response.data?.customerReferenceCode).toBeTruthy(); // Save for later tests createdSubscriptionReferenceCode = response.data?.referenceCode; createdCustomerReferenceCode = response.data?.customerReferenceCode; }); it('should get a subscription', async () => { if (skipIfUnavailable() || !createdSubscriptionReferenceCode) { return; // Skip if feature unavailable or no subscription created } const response = await client.subscription.get(createdSubscriptionReferenceCode); assertSuccessResponse(response); expect(response.data).toBeDefined(); }); it('should list subscriptions', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } const response = await client.subscription.list({ page: 1, count: 10, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.items).toBeInstanceOf(Array); }); it('should list subscriptions with filters', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } const response = await client.subscription.list({ subscriptionStatus: 'ACTIVE', page: 1, count: 10, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); }); it('should initialize subscription with checkout form', async () => { if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { return; // Skip if feature unavailable or no plan created } const uniqueEmail = generateTestEmail(); const response = await client.subscription.initializeCheckoutForm({ locale: 'tr', conversationId: `test-sub-checkout-${Date.now()}`, callbackUrl: 'https://www.merchant.com/callback', pricingPlanReferenceCode: createdPricingPlanReferenceCode, subscriptionInitialStatus: 'ACTIVE', customer: { ...testSubscriptionCustomer, email: uniqueEmail, }, }); assertSuccessResponse(response); expect(response.token).toBeTruthy(); expect(response.checkoutFormContent).toBeTruthy(); expect(response.tokenExpireTime).toBeGreaterThan(0); }); it('should cancel a subscription', async () => { if (skipIfUnavailable() || !createdSubscriptionReferenceCode) { return; // Skip if feature unavailable or no subscription created } const response = await client.subscription.cancel(createdSubscriptionReferenceCode); assertSuccessResponse(response); }); }); // ============================================================================ // Customer Operations Tests // ============================================================================ describe('Customer Operations', () => { it('should list customers', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } const response = await client.subscription.listCustomers({ page: 1, count: 10, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.items).toBeInstanceOf(Array); }); it('should get a customer', async () => { if (skipIfUnavailable() || !createdCustomerReferenceCode) { return; // Skip if feature unavailable or no customer created } const response = await client.subscription.getCustomer(createdCustomerReferenceCode); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.referenceCode).toBe(createdCustomerReferenceCode); }); it('should update a customer', async () => { if (skipIfUnavailable() || !createdCustomerReferenceCode) { return; // Skip if feature unavailable or no customer created } const response = await client.subscription.updateCustomer(createdCustomerReferenceCode, { name: 'Updated John', surname: 'Updated Doe', conversationId: `test-customer-update-${Date.now()}`, }); assertSuccessResponse(response); expect(response.data).toBeDefined(); expect(response.data?.name).toBe('Updated John'); }); }); // ============================================================================ // Card Update Tests // ============================================================================ describe('Card Update Operations', () => { it('should initialize card update checkout form', async () => { if (skipIfUnavailable() || !createdCustomerReferenceCode) { return; // Skip if feature unavailable or no customer created } try { const response = await client.subscription.initializeCardUpdate({ locale: 'tr', conversationId: `test-card-update-${Date.now()}`, callbackUrl: 'https://www.merchant.com/callback', customerReferenceCode: createdCustomerReferenceCode, }); assertSuccessResponse(response); expect(response.token).toBeTruthy(); expect(response.checkoutFormContent).toBeTruthy(); } catch (error) { // Card update requires an active subscription - may fail if subscription was cancelled if (error instanceof IyzicoResponseError) { const errorMessage = error.errorResponse?.errorMessage || ''; if (errorMessage.includes('aktif abonelik bulunamadı')) { console.log('Skipping: No active subscription found for card update test'); return; } } throw error; } }); }); // ============================================================================ // Cleanup - Delete created resources // ============================================================================ describe('Cleanup', () => { it('should delete pricing plan', async () => { if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { console.log('No pricing plan to delete'); return; } try { const response = await client.subscription.deletePricingPlan(createdPricingPlanReferenceCode); assertSuccessResponse(response); } catch (error) { // Deletion may fail if there are active subscriptions - this is expected if (error instanceof IyzicoResponseError) { console.log(`Could not delete pricing plan: ${error.errorResponse?.errorMessage || error.message}`); return; // Don't fail the test for cleanup issues } throw error; } }); it('should delete product', async () => { if (skipIfUnavailable() || !createdProductReferenceCode) { console.log('No product to delete'); return; } try { const response = await client.subscription.deleteProduct(createdProductReferenceCode); assertSuccessResponse(response); } catch (error) { // Deletion may fail if there are active pricing plans/subscriptions - this is expected if (error instanceof IyzicoResponseError) { console.log(`Could not delete product: ${error.errorResponse?.errorMessage || error.message}`); return; // Don't fail the test for cleanup issues } throw error; } }); }); // ============================================================================ // Edge Cases and Error Handling // ============================================================================ describe('Edge Cases - Invalid Inputs', () => { it('should handle non-existent product reference code', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.getProduct('invalid-product-ref-12345') ).rejects.toThrow(IyzicoResponseError); }); it('should handle non-existent pricing plan reference code', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.getPricingPlan('invalid-plan-ref-12345') ).rejects.toThrow(IyzicoResponseError); }); it('should handle non-existent subscription reference code', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.get('invalid-sub-ref-12345') ).rejects.toThrow(IyzicoResponseError); }); it('should handle non-existent customer reference code', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.getCustomer('invalid-customer-ref-12345') ).rejects.toThrow(IyzicoResponseError); }); it('should handle missing required fields in product creation', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.createProduct({ name: '', conversationId: `test-empty-name-${Date.now()}`, }) ).rejects.toThrow(IyzicoResponseError); }); it('should handle invalid pricing plan creation without product', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.createPricingPlan('invalid-product-ref', { ...testPricingPlanCreateRequest, conversationId: `test-invalid-product-${Date.now()}`, }) ).rejects.toThrow(IyzicoResponseError); }); it('should handle subscription initialization with invalid plan', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } const uniqueEmail = generateTestEmail(); await expect( client.subscription.initialize({ locale: 'tr', conversationId: `test-invalid-plan-${Date.now()}`, pricingPlanReferenceCode: 'invalid-plan-ref', subscriptionInitialStatus: 'ACTIVE', customer: { ...testSubscriptionCustomer, email: uniqueEmail, }, paymentCard: testSubscriptionPaymentCard, }) ).rejects.toThrow(IyzicoResponseError); }); it('should handle retry payment with invalid reference', async () => { if (skipIfUnavailable()) { return; // Skip if feature unavailable } await expect( client.subscription.retryPayment({ referenceCode: 'invalid-order-ref-12345', conversationId: `test-invalid-retry-${Date.now()}`, }) ).rejects.toThrow(IyzicoResponseError); }); }); });