272eca04ce
购买/支付(Phase B): - 新 SegSwitch 段控 widget(镜像 .segswitch 原子) - 购买页重构:顶部支付渠道 switch(默认 USDT→加密货币 / 人民币→支付宝),套餐价随渠道分币种显示,删底部弹窗选方式;点购买直达支付页 - 支付页加订单信息卡(套餐/渠道/金额按币种) - models:PayChannel + PayCatalogItem.priceUsdtMicro/priceLabel(ch)/perMonthLabel(ch) 订单(P0,全新): - payment_api.listOrders() + orders_provider(列表 + 详情 family) - OrdersScreen 列表→详情(状态 pill / 可复制订单号 / 继续支付·重新购买) - 导航:NavView.orders + desktop_shell 分支 + account 入口 配套: - l10n +19 getter(支付渠道 4 + 订单 15)×6 语言,l10n 闸绿 - 图标单源迁移 lucide_icons_flutter + pangolin_icons.dart(codegen) - 联系渠道订正(Telegram/邮件 support@yanmeiai.com/官网) + 前端去广告&时长 - 测试:购买 switch 分币种 + 订单列表/空态/详情 + 假 API listOrders;golden 重录 - flutter analyze 0 error,flutter test 226 全绿 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
75 lines
3.0 KiB
Dart
75 lines
3.0 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:pangolin_vpn/services/api_client.dart';
|
|
import 'package:pangolin_vpn/services/auth_api.dart';
|
|
import 'package:pangolin_vpn/models/payment.dart';
|
|
import 'package:pangolin_vpn/services/payment_api.dart';
|
|
|
|
ApiClient _client(MockClient mock) => ApiClient(
|
|
baseUrl: 'http://x',
|
|
getToken: () => 'tok1',
|
|
refresh: () async => false,
|
|
client: mock,
|
|
);
|
|
|
|
void main() {
|
|
test('catalog 解析三档', () async {
|
|
final api = PaymentApi(_client(MockClient((req) async {
|
|
expect(req.url.path, '/v1/pay/catalog');
|
|
return http.Response(
|
|
'{"items":[{"sku":"pro_month","plan":"pro","days":31,"price_minor":2999,"currency":"CNY"},'
|
|
'{"sku":"pro_quarter","plan":"pro","days":92,"price_minor":6888,"currency":"CNY"},'
|
|
'{"sku":"pro_year","plan":"pro","days":366,"price_minor":19999,"currency":"CNY"}]}',
|
|
200);
|
|
})));
|
|
final items = await api.catalog();
|
|
expect(items.length, 3);
|
|
expect(items.first.sku, 'pro_month');
|
|
expect(items.first.priceLabel(PayChannel.cny), '¥29.99');
|
|
});
|
|
|
|
test('createOrder 只传 sku+method+metadata,解析 session', () async {
|
|
final api = PaymentApi(_client(MockClient((req) async {
|
|
expect(req.url.path, '/v1/pay/orders');
|
|
expect(req.body.contains('"amount'), isFalse, reason: '客户端绝不传金额');
|
|
return http.Response(
|
|
'{"order_no":"pay1","session":{"render_type":"crypto_address",'
|
|
'"payload":{"address":"Txx","amount":"4.201234","amount_minor":4201234,'
|
|
'"currency":"USDT","network":"TRC20"},"expires_at":"2026-07-10T12:00:00Z"}}',
|
|
200);
|
|
})));
|
|
final order = await api.createOrder(sku: 'pro_month', method: 'crypto');
|
|
expect(order.orderNo, 'pay1');
|
|
expect(order.session.renderType, 'crypto_address');
|
|
expect(order.session.payload['address'], 'Txx');
|
|
expect(order.session.expiresAt, isNotNull);
|
|
});
|
|
|
|
test('orderStatus 解析 activated/expires_at', () async {
|
|
final api = PaymentApi(_client(MockClient((req) async => http.Response(
|
|
'{"order_no":"pay1","pay_status":"succeeded","activated":true,'
|
|
'"expires_at":"2026-08-10T12:00:00Z"}',
|
|
200))));
|
|
final st = await api.orderStatus('pay1');
|
|
expect(st.activated, isTrue);
|
|
expect(st.payStatus, 'succeeded');
|
|
expect(st.expiresAt, isNotNull);
|
|
});
|
|
|
|
test('retry 409 CURRENCY_MISMATCH 抛带 code 的异常', () async {
|
|
final api = PaymentApi(_client(MockClient((req) async => http.Response(
|
|
'{"code":"CURRENCY_MISMATCH","message_zh":"该支付方式结算币种与订单不符,请重新下单",'
|
|
'"message_en":"mismatch"}',
|
|
409,
|
|
headers: {'content-type': 'application/json; charset=utf-8'}))));
|
|
try {
|
|
await api.retry('pay1', method: 'alipay');
|
|
fail('应抛异常');
|
|
} on AuthApiException catch (e) {
|
|
expect(e.statusCode, 409);
|
|
expect(e.code, 'CURRENCY_MISMATCH');
|
|
}
|
|
});
|
|
}
|