// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/Redemption.sol"; import "../src/Coupon.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICoupon.sol"; contract RedemptionTest is Test { Redemption redemption; Coupon coupon; Compliance compliance; address admin = address(1); address consumer = address(4); bytes32 storeA = keccak256("STORE_A"); bytes32 storeB = keccak256("STORE_B"); function setUp() public { vm.startPrank(admin); compliance = new Compliance(); compliance.initialize(admin); coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); coupon.grantRole(keccak256("FACTORY_ROLE"), admin); redemption = new Redemption(); redemption.initialize(address(coupon), address(compliance), admin); // Grant Redemption contract the SETTLER_ROLE so it can burn coupons coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption)); compliance.setKycLevel(consumer, 1); vm.stopPrank(); } function test_RedeemSuccess() public { uint256 tokenId = _mintCoupon(consumer, 100e6, false); vm.prank(consumer); redemption.redeem(tokenId, storeA); // 券已销毁 vm.expectRevert(); coupon.ownerOf(tokenId); // 记录已创建 Redemption.RedemptionRecord memory record = redemption.getRedemption(0); assertEq(record.tokenId, tokenId); assertEq(record.consumer, consumer); assertEq(record.storeId, storeA); } function test_RedeemExpiredCouponReverts() public { uint256 tokenId = _mintCoupon(consumer, 100e6, false); vm.warp(block.timestamp + 200 days); vm.prank(consumer); vm.expectRevert("Redemption: coupon expired"); redemption.redeem(tokenId, storeA); } function test_RedeemNotOwnerReverts() public { uint256 tokenId = _mintCoupon(consumer, 100e6, false); vm.prank(admin); vm.expectRevert("Redemption: not owner"); redemption.redeem(tokenId, storeA); } function test_RedeemBlacklistedReverts() public { uint256 tokenId = _mintCoupon(consumer, 100e6, false); vm.prank(admin); compliance.addToBlacklist(consumer, "OFAC"); vm.prank(consumer); vm.expectRevert("Redemption: blacklisted"); redemption.redeem(tokenId, storeA); } function test_RedeemStoreRestricted() public { uint256 tokenId = _mintCouponWithStore(consumer, 100e6); // 使用不在列表中的门店 bytes32 storeC = keccak256("STORE_C"); vm.prank(consumer); vm.expectRevert("Redemption: store not allowed"); redemption.redeem(tokenId, storeC); // 使用允许的门店 vm.prank(consumer); redemption.redeem(tokenId, storeA); } function test_TotalRedemptionsIncrement() public { uint256 tokenId1 = _mintCoupon(consumer, 100e6, false); uint256 tokenId2 = _mintCoupon(consumer, 200e6, false); vm.startPrank(consumer); redemption.redeem(tokenId1, storeA); redemption.redeem(tokenId2, storeA); vm.stopPrank(); assertEq(redemption.totalRedemptions(), 2); } // --- Helpers --- function _mintCoupon(address to, uint256 faceValue, bool withStores) internal returns (uint256) { bytes32[] memory stores; if (withStores) { stores = new bytes32[](2); stores[0] = storeA; stores[1] = storeB; } else { stores = new bytes32[](0); } ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: faceValue, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: admin, faceValue: faceValue }); vm.prank(admin); return coupon.mint(to, faceValue, config); } function _mintCouponWithStore(address to, uint256 faceValue) internal returns (uint256) { return _mintCoupon(to, faceValue, true); } }