diff --git a/backend/test/Cases/Platform/Tmall/EntityParse/RefundAttributeParseTest.php b/backend/test/Cases/Platform/Tmall/EntityParse/RefundAttributeParseTest.php new file mode 100644 index 0000000..c2a1c4a --- /dev/null +++ b/backend/test/Cases/Platform/Tmall/EntityParse/RefundAttributeParseTest.php @@ -0,0 +1,434 @@ +parser = $reflection->newInstanceWithoutConstructor(); + } + + // ======================================================== + // 编码解码测试 + // ======================================================== + + /** + * 验证 #3B → :(冒号) 解码正确 + * bgmtc 原始值 "2026-02-24 13#3B47#3B41" 应解码为 "2026-02-24 13:47:41" + */ + public function testDecodeHashEncoding(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('2026-02-24 13:47:41', $result['bgmtc']); + } + + /** + * 验证 #3A → ;(分号) 解码正确 + * SKU 属性分隔符 #3A 应解码为 ; + */ + public function testDecodeHashEncodingSemicolon(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + // SKU 的 properties 使用 ; 分隔后再用 : 切分 + // 原始: 口味#3B橘子味#3A颜色分类#3B维C 250mg + // 解码: 口味:橘子味;颜色分类:维C 250mg + $this->assertSame('橘子味', $result['sku']['properties']['口味']); + $this->assertSame('维C 250mg', $result['sku']['properties']['颜色分类']); + } + + // ======================================================== + // 基本信息 + // ======================================================== + + public function testParseBasicInfo(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('tmall.hk.refund', $result['biz_code']); + $this->assertSame('ali.china.tmall.tmallhk', $result['sdk_code']); + $this->assertSame('NordicNaturalsPro海外旗舰店', $result['shop_name']); + $this->assertSame('refund', $result['workflow_name']); + $this->assertSame('daemon', $result['op_role']); + } + + // ======================================================== + // 商品信息 & SKU 解析 + // ======================================================== + + /** + * 样本A SKU: 5085821404607|口味:橘子味;颜色分类:维C 250mg + */ + public function testParseSkuWithMultipleProperties(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('5085821404607', $result['sku']['sku_id']); + $this->assertCount(2, $result['sku']['properties']); + $this->assertSame('橘子味', $result['sku']['properties']['口味']); + $this->assertSame('维C 250mg', $result['sku']['properties']['颜色分类']); + } + + /** + * 样本B SKU: 5112591851366|颜色分类:120粒 (仅一个属性) + */ + public function testParseSkuWithSingleProperty(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleB); + + $this->assertSame('5112591851366', $result['sku']['sku_id']); + $this->assertCount(1, $result['sku']['properties']); + $this->assertSame('120粒', $result['sku']['properties']['颜色分类']); + } + + public function testParseProductInfo(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + // 购买数量 + $this->assertSame(1, $resultA['item_buy_amount']); + + // 商品单价 (分) + $this->assertSame(39900, $resultA['item_price']); // ¥399.00 + $this->assertSame(119900, $resultB['item_price']); // ¥1,199.00 + + // 类目 + $this->assertSame('50026924', $resultA['leaves_cat']); + $this->assertSame('50026800', $resultA['root_cat']); + $this->assertFalse($resultA['is_virtual']); + } + + // ======================================================== + // 退款金额 + // ======================================================== + + public function testParseRefundAmountsSampleA(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame(17700, $result['apply_init_refund_fee']); // 申请退款 ¥177.00 + $this->assertSame(17700, $result['ex_max_refund_fee']); // 最大可退 ¥177.00 + $this->assertSame(17700, $result['online_refund_fee']); // 在线退款 ¥177.00 + $this->assertSame(0, $result['refund_post_fee']); + $this->assertSame(0, $result['main_order_post_fee']); + $this->assertSame(0, $result['to_seller_fee']); + } + + public function testParseRefundAmountsSampleB(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleB); + + $this->assertSame(66900, $result['apply_init_refund_fee']); // 申请退款 ¥669.00 + $this->assertSame(66900, $result['ex_max_refund_fee']); // 最大可退 ¥669.00 + $this->assertSame(66900, $result['online_refund_fee']); // 在线退款 ¥669.00 + } + + // ======================================================== + // 退款原因与类型 + // ======================================================== + + public function testParseRefundReasonAndType(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('不喜欢/不想要', $result['apply_reason_text']); + $this->assertSame('175001', $result['apply_text_id']); + $this->assertSame(1, $result['dispute_request']); + $this->assertSame(4, $result['dispute_trade_status']); + $this->assertSame('InterceptUnconsignRefund', $result['dbt']); // 拦截未发货退款 + } + + // ======================================================== + // 退款特征标识 (闪电退款判定的关键字段) + // ======================================================== + + /** + * clj_zero_second_refund = 1 表示0秒闪电退款 + * 这是 fixInstantRefundOrders() 的判定依据 + */ + public function testParseFlashRefundFlags(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + // 两笔样本都是闪电退款 + $this->assertTrue($resultA['clj_zero_second_refund']); + $this->assertTrue($resultB['clj_zero_second_refund']); + + $this->assertTrue($resultA['tmg_simple_zero_refund']); + $this->assertTrue($resultA['warehouse_refund']); + $this->assertFalse($resultA['part_refund']); + $this->assertTrue($resultA['percent_refund']); + $this->assertSame('timeoutrefund^', $resultA['products']); + } + + // ======================================================== + // 资金流 (fundFlowInfo) + // ======================================================== + + /** + * 样本A 有 fundFlowInfo: 支付宝 ¥154.89 + 优惠 ¥22.11 + */ + public function testParseFundFlowPresent(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertCount(2, $result['fund_flow']); + + // 支付宝现金 + $this->assertSame('CASH', $result['fund_flow'][0]['type']); + $this->assertSame('ALIPAY_FUND', $result['fund_flow'][0]['group']); + $this->assertSame('支付宝', $result['fund_flow'][0]['name']); + $this->assertSame(15489, $result['fund_flow'][0]['amount']); // ¥154.89 + + // 优惠资产 + $this->assertSame('OTHER_ASSETS', $result['fund_flow'][1]['type']); + $this->assertSame('OTHER_ASSETS_GROUP', $result['fund_flow'][1]['group']); + $this->assertSame('优惠', $result['fund_flow'][1]['name']); + $this->assertSame(2211, $result['fund_flow'][1]['amount']); // ¥22.11 + + $this->assertSame('alipay', $result['pay_mode']); + $this->assertSame('system', $result['pay_lock']); + } + + /** + * 样本B 没有 fundFlowInfo,fund_flow 应为空数组 + */ + public function testParseFundFlowAbsent(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleB); + + $this->assertSame([], $result['fund_flow']); + $this->assertSame('alipay', $result['pay_mode']); + } + + // ======================================================== + // 优惠信息 (pmtR / threshold) + // ======================================================== + + public function testParsePromotions(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + // 样本A: 优惠券 ¥22.11 + $this->assertCount(1, $resultA['promotions']); + $this->assertSame('TAPP_USERCOUPON_SP', $resultA['promotions'][0]['type']); + $this->assertSame(2211, $resultA['promotions'][0]['amount']); + + // 样本B: 优惠券 ¥65.89 + $this->assertCount(1, $resultB['promotions']); + $this->assertSame('TAPP_USERCOUPON_SP', $resultB['promotions'][0]['type']); + $this->assertSame(6589, $resultB['promotions'][0]['amount']); + + $this->assertTrue($resultA['has_threshold_coupon']); + } + + public function testParseThresholdInstruction(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertIsArray($result['threshold_instruction']); + $this->assertArrayHasKey('TAPP_USERCOUPON_SP', $result['threshold_instruction']); + } + + // ======================================================== + // 价保 + // ======================================================== + + public function testParsePriceProtection(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + // 样本A: 2026-02-24 13:47:45 ~ 2026-03-16 23:59:59 + $this->assertSame('2026-02-24 13:47:45', $resultA['price_protection']['start']); + $this->assertSame('2026-03-16 23:59:59', $resultA['price_protection']['end']); + + // 样本B: 2026-02-24 00:04:01 ~ 2026-03-16 23:59:59 + $this->assertSame('2026-02-24 00:04:01', $resultB['price_protection']['start']); + $this->assertSame('2026-03-16 23:59:59', $resultB['price_protection']['end']); + } + + // ======================================================== + // 物流拦截 + // ======================================================== + + public function testParseInterceptInfo(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('clj', $result['intercept_type']); // 菜鸟拦截 + $this->assertSame(2, $result['intercept_status']); + $this->assertTrue($result['ability_success_flag']); + $this->assertSame(2, $result['warehouse_intercept_op']); + } + + /** + * interceptItemListResult 是编码后的 JSON + * 解码后应为包含拦截明细的数组 + */ + public function testParseInterceptItemsJson(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertIsArray($result['intercept_items']); + $this->assertCount(1, $result['intercept_items']); + $this->assertSame(5081613120736477027, $result['intercept_items'][0]['subBizOrderId']); + $this->assertSame('INTERCEPT_SUCCESS', $result['intercept_items'][0]['logisticInterceptEnum']); + $this->assertSame('Feb 24, 2026 1:48:02 PM', $result['intercept_items'][0]['interceptDate']); + } + + public function testParseInterceptItemsSampleB(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleB); + + $this->assertIsArray($result['intercept_items']); + $this->assertCount(1, $result['intercept_items']); + $this->assertSame('INTERCEPT_SUCCESS', $result['intercept_items'][0]['logisticInterceptEnum']); + $this->assertSame('Feb 24, 2026 12:04:41 AM', $result['intercept_items'][0]['interceptDate']); + } + + // ======================================================== + // 卖家 / 买家 + // ======================================================== + + public function testParseSellerBuyerInfo(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + $this->assertTrue($resultA['seller_batch']); + $this->assertSame(0, $resultA['seller_audit']); + $this->assertFalse($resultA['has_seller_memo']); + $this->assertSame('clj', $resultA['agree_source']); // 菜鸟自动同意 + + // 买家信用等级不同 + $this->assertSame(4, $resultA['user_credit']); + $this->assertSame(2, $resultB['user_credit']); + } + + // ======================================================== + // 其他字段 + // ======================================================== + + public function testParseOtherFields(): void + { + $result = $this->parser->parseRefundItemAttribute($this->sampleA); + + $this->assertSame('5081613120736477027', $result['channel_sub_order_id']); + $this->assertFalse($result['last_order']); + $this->assertTrue($result['b2c']); + } + + // ======================================================== + // 两笔样本的差异性对比 + // ======================================================== + + /** + * 确认两笔样本的关键差异点 + */ + public function testSampleDifferences(): void + { + $resultA = $this->parser->parseRefundItemAttribute($this->sampleA); + $resultB = $this->parser->parseRefundItemAttribute($this->sampleB); + + // 金额差异 + $this->assertSame(17700, $resultA['apply_init_refund_fee']); // ¥177 + $this->assertSame(66900, $resultB['apply_init_refund_fee']); // ¥669 + + // 商品单价差异 + $this->assertSame(39900, $resultA['item_price']); // ¥399 + $this->assertSame(119900, $resultB['item_price']); // ¥1,199 + + // 资金流: A有,B无 + $this->assertCount(2, $resultA['fund_flow']); + $this->assertCount(0, $resultB['fund_flow']); + + // 优惠券金额差异 + $this->assertSame(2211, $resultA['promotions'][0]['amount']); // ¥22.11 + $this->assertSame(6589, $resultB['promotions'][0]['amount']); // ¥65.89 + + // 类目不同 + $this->assertSame('50026924', $resultA['leaves_cat']); + $this->assertSame('50026872', $resultB['leaves_cat']); + + // 共同特征: 都是闪电退款 + 仓库拦截成功 + $this->assertTrue($resultA['clj_zero_second_refund']); + $this->assertTrue($resultB['clj_zero_second_refund']); + } + + // ======================================================== + // 边界情况 + // ======================================================== + + public function testParseEmptyString(): void + { + $result = $this->parser->parseRefundItemAttribute(''); + + $this->assertSame([], $result); + } + + public function testParseSingleField(): void + { + $result = $this->parser->parseRefundItemAttribute(';bizCode:tmall.hk.refund;'); + + $this->assertSame('tmall.hk.refund', $result['biz_code']); + // 缺失字段使用默认值 + $this->assertNull($result['shop_name']); + $this->assertSame(0, $result['item_price']); + $this->assertFalse($result['clj_zero_second_refund']); + $this->assertSame([], $result['fund_flow']); + $this->assertSame([], $result['promotions']); + $this->assertNull($result['price_protection']); + $this->assertNull($result['sku']); + } + + public function testParseNoLeadingSemicolon(): void + { + // attribute 可能不以 ; 开头 + $result = $this->parser->parseRefundItemAttribute('bizCode:tmall.refund;shopName:test'); + + $this->assertSame('tmall.refund', $result['biz_code']); + } +}