feat(app): 接入金币充值与会员商品下单

This commit is contained in:
Zhou Weidong committed 2026-10-20 04:32:03 +08:00
1 parent ec2db6842e
commit 730e38622f
21 files changed
+1325 -1

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// Author: fruit.com.cn
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/commerce_api.dart';
import '../support/fake_http_adapter.dart';
void main() {
test('should_request_coin_packs_when_type_is_given', () async {
final adapter = FakeHttpAdapter(
(options) async => FakeHttpResponse(
jsonEncode({
'code': 0,
'message': 'ok',
'data': [
{
'id': '2111012753262120962',
'code': 'coin_550',
'name': '550 Coins',
'type': 'COIN_PACK',
'coinAmount': '500',
'bonusCoin': '50',
'totalCoin': '550',
'displayPrice': 'Rp 15.000',
'currency': 'IDR',
'vipDays': 0,
},
],
}),
),
);
final api = CommerceApi(
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
);
final products = await api.products(type: 'COIN_PACK');
final request = adapter.requests.single;
expect(request.method, 'GET');
expect(request.uri.path, '/app/products');
expect(request.queryParameters['type'], 'COIN_PACK');
expect(products.single.id, '2111012753262120962');
expect(products.single.coinAmount, 500);
expect(products.single.bonusCoin, 50);
expect(products.single.displayPrice, 'Rp 15.000');
});
test('should_post_product_id_when_creating_an_order', () async {
final adapter = FakeHttpAdapter(
(options) async => FakeHttpResponse(
jsonEncode({
'code': 0,
'message': 'ok',
'data': {
'id': '2111012753262120999',
'orderNo': 'ORD20261020001',
'productId': '2111012753262120962',
'productName': '550 Coins',
'type': 'COIN_PACK',
'coinAmount': '550',
'status': 'CREATED',
'createdAt': '2026-10-20 10:00:00',
},
}),
),
);
final api = CommerceApi(
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
);
final order = await api.createOrder('2111012753262120962');
final request = adapter.requests.single;
expect(request.method, 'POST');
expect(request.uri.path, '/app/orders');
final body = request.data is String
? jsonDecode(request.data as String)
: request.data;
expect(body, {'productId': '2111012753262120962'});
expect(order.orderNo, 'ORD20261020001');
expect(order.coinAmount, 550);
expect(order.status, 'CREATED');
});
}
+108
View File
@@ -0,0 +1,108 @@
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/order.dart';
import 'package:fruitshorts/core/data/models/product.dart';
import 'package:fruitshorts/core/data/remote/api_exception.dart';
import 'package:fruitshorts/core/data/repositories/store_repository.dart';
import 'package:fruitshorts/core/data/services/store_controller.dart';
void main() {
test('should_expose_coin_packs_when_store_loads', () async {
final repository = _FakeStoreRepository(
products: [_product('550 Coins', coinAmount: 500, bonusCoin: 50)],
);
final controller = StoreController(
repository,
type: StoreController.coinPackType,
);
await controller.load();
expect(repository.requestedTypes, [StoreController.coinPackType]);
expect(controller.products.single.name, '550 Coins');
expect(controller.error, isNull);
});
test('should_create_order_and_keep_purchase_when_buying', () async {
final repository = _FakeStoreRepository(
products: [_product('550 Coins')],
order: const AppOrder(
id: '99',
orderNo: 'ORD1',
productName: '550 Coins',
coinAmount: 550,
status: 'CREATED',
),
);
final controller = StoreController(
repository,
type: StoreController.coinPackType,
);
await controller.load();
final order = await controller.buy(controller.products.single);
expect(repository.orderedProductIds, ['2111012753262120962']);
expect(order?.orderNo, 'ORD1');
expect(controller.purchaseError, isNull);
expect(controller.purchasingProductId, isNull);
});
test('should_expose_server_message_when_order_is_rejected', () async {
final repository = _FakeStoreRepository(
products: [_product('550 Coins')],
orderError: const ApiException(400, 'Product is offline'),
);
final controller = StoreController(
repository,
type: StoreController.coinPackType,
);
await controller.load();
final order = await controller.buy(controller.products.single);
expect(order, isNull);
expect(controller.purchaseError, 'Product is offline');
});
}
AppProduct _product(String name, {int coinAmount = 550, int bonusCoin = 0}) =>
AppProduct(
id: '2111012753262120962',
code: 'coin_550',
name: name,
type: StoreController.coinPackType,
coinAmount: coinAmount,
bonusCoin: bonusCoin,
totalCoin: coinAmount + bonusCoin,
displayPrice: 'Rp 15.000',
vipDays: 0,
);
class _FakeStoreRepository implements StoreRepository {
_FakeStoreRepository({
this.products = const [],
this.order,
this.orderError,
});
final List<AppProduct> products;
final AppOrder? order;
final ApiException? orderError;
final List<String?> requestedTypes = [];
final List<String> orderedProductIds = [];
@override
Future<List<AppProduct>> fetchProducts({String? type}) async {
requestedTypes.add(type);
return products;
}
@override
Future<AppOrder> createOrder(String productId) async {
orderedProductIds.add(productId);
if (orderError != null) throw orderError!;
return order!;
}
}
+105
View File
@@ -0,0 +1,105 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/order.dart';
import 'package:fruitshorts/core/data/models/product.dart';
import 'package:fruitshorts/core/data/repositories/store_repository.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/store/recharge_page.dart';
import 'package:fruitshorts/pages/store/vip_page.dart';
void main() {
testWidgets('should_show_coin_packs_and_create_order_when_buying', (
tester,
) async {
final repository = _FakeStoreRepository();
await tester.pumpWidget(_app(RechargePage(repository: repository)));
await tester.pumpAndSettle();
expect(find.text('550 Coins'), findsOneWidget);
expect(find.textContaining('500 coins'), findsOneWidget);
expect(find.textContaining('+50 bonus'), findsOneWidget);
expect(find.text('Rp 15.000'), findsOneWidget);
await tester.tap(find.byKey(const Key('store-buy-pack-1')));
await tester.pumpAndSettle();
expect(repository.requestedTypes, ['COIN_PACK']);
expect(repository.orderedProductIds, ['pack-1']);
expect(find.textContaining('Order ORD1 created.'), findsOneWidget);
});
testWidgets('should_show_vip_tiers_and_create_order_when_joining', (
tester,
) async {
final repository = _FakeStoreRepository();
await tester.pumpWidget(_app(VipPage(repository: repository)));
await tester.pumpAndSettle();
expect(find.text('30-day VIP'), findsOneWidget);
expect(find.text('Rp 99.000'), findsOneWidget);
await tester.tap(find.byKey(const Key('store-buy-vip-1')));
await tester.pumpAndSettle();
expect(repository.requestedTypes, ['VIP']);
expect(repository.orderedProductIds, ['vip-1']);
expect(find.textContaining('Order ORD1 created.'), findsOneWidget);
});
}
Widget _app(Widget home) => MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: home,
);
class _FakeStoreRepository implements StoreRepository {
final List<String?> requestedTypes = [];
final List<String> orderedProductIds = [];
@override
Future<List<AppProduct>> fetchProducts({String? type}) async {
requestedTypes.add(type);
if (type == AppProduct.vipType) {
return const [
AppProduct(
id: 'vip-1',
code: 'vip_30',
name: 'Monthly VIP',
type: AppProduct.vipType,
displayPrice: 'Rp 99.000',
vipDays: 30,
),
];
}
return const [
AppProduct(
id: 'pack-1',
code: 'coin_550',
name: '550 Coins',
type: AppProduct.coinPackType,
coinAmount: 500,
bonusCoin: 50,
totalCoin: 550,
displayPrice: 'Rp 15.000',
),
];
}
@override
Future<AppOrder> createOrder(String productId) async {
orderedProductIds.add(productId);
return const AppOrder(
id: 'order-1',
orderNo: 'ORD1',
productName: 'Coin pack',
coinAmount: 550,
status: AppOrder.created,
);
}
}