diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/controller/points/TbPointsExchangeRecordController.java b/eladmin-system/src/main/java/cn/ysk/cashier/controller/points/TbPointsExchangeRecordController.java index 2ce213f2..be800ddd 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/controller/points/TbPointsExchangeRecordController.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/controller/points/TbPointsExchangeRecordController.java @@ -46,11 +46,18 @@ public class TbPointsExchangeRecordController { return ResponseEntity.ok().build(); } - @PostMapping("exchange") - @ApiOperation("兑换") - public ResponseEntity exchange(@RequestBody TbPointsExchangeRecord record) { - TbPointsExchangeRecord data = tbPointsExchangeRecordService.exchange(record); - return ResponseEntity.ok().body(data); + @PostMapping("cancel") + @ApiOperation("取消") + public ResponseEntity cancel(@RequestBody TbPointsExchangeRecord record) { + tbPointsExchangeRecordService.cancel(record); + return ResponseEntity.ok().build(); + } + + @PostMapping("refund") + @ApiOperation("退单") + public ResponseEntity refund(@RequestBody TbPointsExchangeRecord record) { + tbPointsExchangeRecordService.refund(record); + return ResponseEntity.ok().build(); } @GetMapping("total") diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/dto/points/OrderDeductionPointsDTO.java b/eladmin-system/src/main/java/cn/ysk/cashier/dto/points/OrderDeductionPointsDTO.java index 6ac6ebda..d3bc3ae2 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/dto/points/OrderDeductionPointsDTO.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/dto/points/OrderDeductionPointsDTO.java @@ -24,6 +24,10 @@ public class OrderDeductionPointsDTO { * 根据策略计算出的最多可以抵扣的金额 */ private BigDecimal maxDeductionAmount; + /** + * 下单实付抵扣门槛(实付金额不低于这个值) + */ + private BigDecimal minPaymentAmount; /** * 下单积分抵扣门槛(每次使用不低于这个值) */ diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPoints.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPoints.java index 08072dae..475ecce7 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPoints.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPoints.java @@ -57,11 +57,16 @@ public class TbMemberPoints { * 最近一次积分变动时间 */ @TableField("last_points_change_time") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date lastPointsChangeTime; /** * 最近一次浮动积分 */ @TableField("last_float_points") private Integer lastFloatPoints; + /** + * 是否会员 1-是 0-否 + */ + @TableField("is_vip") + private Integer vip; } \ No newline at end of file diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPointsLog.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPointsLog.java index c86808ed..c1ed711b 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPointsLog.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbMemberPointsLog.java @@ -65,6 +65,6 @@ public class TbMemberPointsLog { /** * 创建时间 */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; } \ No newline at end of file diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsBasicSetting.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsBasicSetting.java index daa71be3..a7bce867 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsBasicSetting.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsBasicSetting.java @@ -35,6 +35,10 @@ public class TbPointsBasicSetting { * 开启消费赠送积分 1-开启 0-关闭 */ private Integer enableRewards; + /** + * 赠积分适用群体 all-全部 vip-仅会员 + */ + private String rewardsGroup; /** * 每消费xx元赠送1积分 */ @@ -44,9 +48,13 @@ public class TbPointsBasicSetting { */ private Integer enableDeduction; /** - * 下单积分抵扣门槛 + * 抵扣适用群体 all-全部 vip-仅会员 + */ + private String deductionGroup; + /** + * 下单实付抵扣门槛 */ - private Integer minDeductionPoint; + private BigDecimal minPaymentAmount; /** * 下单最高抵扣比例 */ @@ -66,6 +74,6 @@ public class TbPointsBasicSetting { /** * 创建时间 */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; } \ No newline at end of file diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsExchangeRecord.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsExchangeRecord.java index 07c18390..ff1612b6 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsExchangeRecord.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsExchangeRecord.java @@ -2,6 +2,8 @@ package cn.ysk.cashier.mybatis.entity; import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -78,18 +80,48 @@ public class TbPointsExchangeRecord { */ private String couponCode; /** - * 状态 waiting-待自取 done-已完成 + * 支付平台订单号 + */ + private String payOrderId; + /** + * 渠道订单号(微信/支付宝订单号) + */ + private String channelTradeNo; + /** + * 支付方式 积分支付/积分+微信/积分+支付宝 + */ + private String payMethod; + /** + * 支付类型 POINTS-积分 WECHAT-微信 ALIPAY-支付宝 UNIONPAY-银联云闪付 + */ + private String payType; + /** + * 状态 unpaid-待支付 waiting-待自取 done-已完成 cancel-已取消 */ private String status; + /** + * 取消/退款原因 + */ + private String cancelOrRefundReason; + /** + * 取消/退款时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date cancelOrRefundTime; + /** + * 实际支付时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date payTime; /** * 创建时间(下单时间) */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; /** * 更新时间(核销时间) */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date updateTime; @TableField(value = "count(*)", select = false, insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) @@ -97,4 +129,24 @@ public class TbPointsExchangeRecord { @TableField(value = "sum(extra_payment_amount)", select = false, insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER) private BigDecimal totalAmount; + + /** + * 用户ip + */ + @JsonIgnore + @TableField(exist = false) + private String ip; + + /** + * 微信openId/支付完userId + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @TableField(exist = false) + private String openId; + + /** + * 拉起支付所需信息 + */ + @TableField(exist = false) + private String payInfo; } \ No newline at end of file diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsGoodsSetting.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsGoodsSetting.java index 666dbc69..172da2ba 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsGoodsSetting.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/entity/TbPointsGoodsSetting.java @@ -75,12 +75,12 @@ public class TbPointsGoodsSetting { /** * 创建时间 */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; /** * 更新时间 */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date updateTime; /** * 逻辑删除标志 1-是 0-否 diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/TbPointsExchangeRecordService.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/TbPointsExchangeRecordService.java index af21f22e..3482ddc9 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/TbPointsExchangeRecordService.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/TbPointsExchangeRecordService.java @@ -19,7 +19,9 @@ public interface TbPointsExchangeRecordService extends IService total(Map params); } \ No newline at end of file diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbMemberPointsServiceImpl.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbMemberPointsServiceImpl.java index d9c42ad5..b2aa76c1 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbMemberPointsServiceImpl.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbMemberPointsServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.map.MapProxy; import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.ysk.cashier.dto.points.OrderDeductionPointsDTO; import cn.ysk.cashier.exception.BadRequestException; @@ -82,7 +83,7 @@ public class TbMemberPointsServiceImpl extends ServiceImpl core.getMaxUsablePoints()) { throw new BadRequestException(StrUtil.format("使用积分不能超过最大使用限制{}", core.getMaxUsablePoints())); } - BigDecimal mul = NumberUtil.mul(new BigDecimal("0.01"), core.getEquivalentPoints()); - int minPoints = NumberUtil.round(mul, 0, RoundingMode.CEILING).intValue(); - if (points < minPoints) { - throw new BadRequestException(StrUtil.format("使用积分不能低于{}(0.01元)", minPoints)); - } BigDecimal money = NumberUtil.mul(points, NumberUtil.div(BigDecimal.ONE, core.getEquivalentPoints())); - return NumberUtil.roundDown(money, 2); + BigDecimal maxDeductionAmount = NumberUtil.roundDown(money, 2); + if (NumberUtil.isGreater(maxDeductionAmount, core.getMaxDeductionAmount())) { + return core.getMaxDeductionAmount(); + } + return maxDeductionAmount; } @Override @@ -270,6 +275,17 @@ public class TbMemberPointsServiceImpl extends ServiceImpllambdaQuery().eq(TbPointsBasicSetting::getShopId, entity.getShopId())); super.save(entity); diff --git a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbPointsExchangeRecordServiceImpl.java b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbPointsExchangeRecordServiceImpl.java index a4002001..28871aab 100644 --- a/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbPointsExchangeRecordServiceImpl.java +++ b/eladmin-system/src/main/java/cn/ysk/cashier/mybatis/service/impl/TbPointsExchangeRecordServiceImpl.java @@ -2,22 +2,29 @@ package cn.ysk.cashier.mybatis.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.lang.Snowflake; import cn.hutool.core.map.MapProxy; -import cn.hutool.core.util.IdUtil; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import cn.ysk.cashier.exception.BadRequestException; -import cn.ysk.cashier.mybatis.entity.*; +import cn.ysk.cashier.mybatis.entity.TbMemberPoints; +import cn.ysk.cashier.mybatis.entity.TbMemberPointsLog; +import cn.ysk.cashier.mybatis.entity.TbPointsExchangeRecord; +import cn.ysk.cashier.mybatis.entity.TbPointsGoodsSetting; import cn.ysk.cashier.mybatis.mapper.*; import cn.ysk.cashier.mybatis.service.TbPointsExchangeRecordService; +import cn.ysk.cashier.pojo.shop.TbMerchantThirdApply; +import cn.ysk.cashier.pojo.shop.TbShopInfo; +import cn.ysk.cashier.repository.shop.TbMerchantThirdApplyRepository; +import cn.ysk.cashier.thirdpay.resp.OrderReturnResp; +import cn.ysk.cashier.thirdpay.resp.PublicResp; +import cn.ysk.cashier.thirdpay.service.ThirdPayService; import cn.ysk.cashier.utils.PageUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,6 +43,17 @@ import java.util.Map; @Service public class TbPointsExchangeRecordServiceImpl extends ServiceImpl implements TbPointsExchangeRecordService { + private static final Map payMethod = MapUtil.builder("POINTS", "积分支付") + .put("WECHAT", "积分+微信") + .put("ALIPAY", "积分+支付宝") + .build(); + + @Value("${thirdPay.url}") + private String thirdUrl; + + @Value("${thirdPay.pointsGoodsOrderCallBack}") + private String pointsGoodsOrderCallBack; + @Resource private TbPointsBasicSettingMapper tbPointsBasicSettingMapper; @@ -48,6 +66,14 @@ public class TbPointsExchangeRecordServiceImpl extends ServiceImpl getWrapper(Map params) { MapProxy mapProxy = MapProxy.create(params); String keywords = mapProxy.getStr("keywords"); @@ -89,6 +115,7 @@ public class TbPointsExchangeRecordServiceImpl extends ServiceImpllambdaQuery().eq(TbPointsBasicSetting::getShopId, record.getShopId())); - if (basic == null) { - throw new BadRequestException("未配置积分锁客基本设置"); - } - if (basic.getEnablePointsMall() != 1) { - throw new BadRequestException("积分商城未开启"); - } - TbPointsGoodsSetting goods = tbPointsGoodsSettingMapper.selectById(record.getPointsGoodsId()); + TbPointsGoodsSetting goods = tbPointsGoodsSettingMapper.selectById(entity.getPointsGoodsId()); if (goods == null) { - throw new BadRequestException("兑换的商品信息不存在"); + throw new BadRequestException("积分商品不存在"); } - if (goods.getDelFlag() == 1) { - throw new BadRequestException("兑换的商品信息不存在"); - } - record.setPointsGoodsName(goods.getGoodsName()); - record.setGoodsImageUrl(goods.getGoodsImageUrl()); - - Integer status = goods.getStatus(); - if (status != 1) { - throw new BadRequestException("兑换的商品已下架"); - } - Integer quantity = goods.getQuantity(); - if (quantity <= 0) { - throw new BadRequestException("兑换的商品库存不足"); - } - TbMemberPoints memberPoints = tbMemberPointsMapper.selectOne(Wrappers.lambdaQuery().eq(TbMemberPoints::getMemberId, record.getMemberId())); - if (memberPoints == null) { - throw new BadRequestException("该会员积分不足无法兑换这个商品"); - } - Integer accountPoints = memberPoints.getAccountPoints(); - Integer requiredPoints = goods.getRequiredPoints(); - if (accountPoints < requiredPoints) { - throw new BadRequestException("该会员积分不足无法兑换这个商品"); - } - BigDecimal extraPrice = goods.getExtraPrice(); - record.setExtraPaymentAmount(extraPrice); - record.setSpendPoints(requiredPoints); - Snowflake seqNo = IdUtil.getSnowflake(0, 0); - String orderNo = DateUtil.format(new Date(), "yyyyMMddHH") + StrUtil.subSuf(seqNo.nextIdStr(), -12); - record.setOrderNo(orderNo); - record.setCouponCode(IdUtil.getSnowflakeNextIdStr()); - record.setStatus("waiting"); - record.setCreateTime(new Date()); - // 生成订单 - super.save(record); - // 扣减积分 - memberPoints.setAccountPoints(accountPoints - requiredPoints); - memberPoints.setLastPointsChangeTime(new Date()); - memberPoints.setLastFloatPoints(-requiredPoints); - tbMemberPointsMapper.updateById(memberPoints); - // 扣减库存 - goods.setQuantity(quantity - 1); - goods.setUpdateTime(new Date()); - tbPointsGoodsSettingMapper.updateById(goods); - // 记录积分浮动流水 - TbMemberPointsLog log = new TbMemberPointsLog(); - log.setShopId(record.getShopId()); - log.setMemberId(record.getMemberId()); - log.setMemberName(record.getMemberName()); - log.setMobile(record.getMobile()); - log.setAvatarUrl(record.getAvatarUrl()); - log.setContent(StrUtil.format("兑换商品:{} * {}", record.getPointsGoodsName(), "1")); - log.setFloatType("subtract"); - log.setFloatPoints(-requiredPoints); - log.setCreateTime(new Date()); - tbMemberPointsLogMapper.insert(log); // 更新累计兑换数量 goods.setTotalExchangeCount(goods.getTotalExchangeCount() + 1); goods.setUpdateTime(new Date()); tbPointsGoodsSettingMapper.updateById(goods); - return record; + } + + @Override + public void cancel(TbPointsExchangeRecord record) { + if (record.getId() == null) { + throw new BadRequestException("订单ID不能为空"); + } + TbPointsExchangeRecord entity = super.getById(record.getId()); + if (entity == null) { + throw new BadRequestException("订单不存在"); + } + if (!"unpaid".equals(entity.getStatus())) { + throw new BadRequestException("当前订单状态不支持取消"); + } + entity.setStatus("cancel"); + entity.setCancelOrRefundReason(record.getCancelOrRefundReason()); + entity.setCancelOrRefundTime(new Date()); + super.updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refund(TbPointsExchangeRecord record) { + if (record.getId() == null) { + throw new BadRequestException("订单ID不能为空"); + } + TbPointsExchangeRecord entity = super.getById(record.getId()); + if (entity == null) { + throw new BadRequestException("订单不存在"); + } + if (!"waiting".equals(entity.getStatus())) { + throw new BadRequestException("当前订单状态不支持退款"); + } + // 先退积分 + entity.setStatus("cancel"); + entity.setCancelOrRefundReason(record.getCancelOrRefundReason()); + entity.setCancelOrRefundTime(new Date()); + super.updateById(entity); + TbMemberPoints memberPoints = tbMemberPointsMapper.selectOne(Wrappers.lambdaQuery().eq(TbMemberPoints::getMemberId, entity.getMemberId())); + if (memberPoints == null) { + throw new BadRequestException("会员信息不存在"); + } + memberPoints.setAccountPoints(memberPoints.getAccountPoints() + entity.getSpendPoints()); + memberPoints.setLastPointsChangeTime(new Date()); + memberPoints.setLastFloatPoints(entity.getSpendPoints()); + tbMemberPointsMapper.updateById(memberPoints); + // 回滚库存 + TbPointsGoodsSetting goods = tbPointsGoodsSettingMapper.selectById(entity.getPointsGoodsId()); + if (goods == null) { + throw new BadRequestException("积分商品不存在"); + } + goods.setQuantity(goods.getQuantity() + 1); + goods.setUpdateTime(new Date()); + tbPointsGoodsSettingMapper.updateById(goods); + // 记录积分浮动流水 + TbMemberPointsLog log = new TbMemberPointsLog(); + log.setShopId(entity.getShopId()); + log.setMemberId(entity.getMemberId()); + log.setMemberName(entity.getMemberName()); + log.setMobile(entity.getMobile()); + log.setAvatarUrl(entity.getAvatarUrl()); + log.setContent(StrUtil.format("(退单)兑换商品:{} * {}", entity.getPointsGoodsName(), "1")); + log.setFloatType("add"); + log.setFloatPoints(+entity.getSpendPoints()); + log.setCreateTime(new Date()); + tbMemberPointsLogMapper.insert(log); + // 如果额外付款了则需要退款 + if (NumberUtil.equals(entity.getExtraPaymentAmount(), BigDecimal.ZERO)) { + return; + } + // 需要额外支付 + TbShopInfo shopInfo = mpShopInfoMapper.selectById(entity.getShopId()); + if (shopInfo == null) { + throw new BadRequestException("店铺信息不存在"); + } + TbMerchantThirdApply thirdApply = tbMerchantThirdApplyRepository.getById(Integer.valueOf(shopInfo.getMerchantId())); + if (thirdApply == null) { + throw new BadRequestException("支付通道不存在"); + } + if ("alipay".equalsIgnoreCase(entity.getPayType()) && StrUtil.isBlank(thirdApply.getAlipaySmallAppid())) { + throw new BadRequestException("店铺未配置支付宝小程序appId"); + } + // 准备退款参数 + // 应用ID + String appId = thirdApply.getAppId(); + String appToken = thirdApply.getAppToken(); + // 交易金额 单位分 100 表示1元 + long amount = NumberUtil.mul(record.getExtraPaymentAmount(), new BigDecimal("100")).longValue(); + // 退款订单号 + String mchRefundNo = entity.getOrderNo(); + PublicResp publicResp; + try { + publicResp = thirdPayService.returnOrder(thirdUrl, appId, mchRefundNo, entity.getPayOrderId(), null, entity.getCancelOrRefundReason(), amount, null, null, appToken); + } catch (Exception e) { + super.log.error("发起退款失败:", e); + throw new BadRequestException(StrUtil.format("发起退款失败:{}", e.getMessage())); + } + if (publicResp == null) { + throw new BadRequestException("发起退款失败:无响应"); + } + if (!"000000".equals(publicResp.getCode())) { + throw new BadRequestException(publicResp.getMsg()); + } + OrderReturnResp returnResp = publicResp.getObjData(); + if (!"SUCCESS".equals(returnResp.getState())) { + throw new BadRequestException(StrUtil.format("退款失败原因:{}", returnResp.getState())); + } } @Override