create order. add paytype: coin
This commit is contained in:
parent
b35e2ac7a3
commit
8bf8e9357f
|
@ -32,11 +32,13 @@ const (
|
||||||
PayTypeWxpayNative = "wxpay_native" // 微信 native
|
PayTypeWxpayNative = "wxpay_native" // 微信 native
|
||||||
PayTypeWxpayJsapi = "wxpay_jsapi" // 微信 jsapi
|
PayTypeWxpayJsapi = "wxpay_jsapi" // 微信 jsapi
|
||||||
PayTypeWxpayH5 = "wxpay_h5" // 微信支付 h5
|
PayTypeWxpayH5 = "wxpay_h5" // 微信支付 h5
|
||||||
|
PayTypeCoin = "coin" // 金币支付
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CallBackPayTypeAlipay = "alipay"
|
CallBackPayTypeAlipay = "alipay"
|
||||||
CallBackPayTypeWxpay = "wxpay"
|
CallBackPayTypeWxpay = "wxpay"
|
||||||
|
CallBackPayTypeCoin = "coin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -296,6 +296,8 @@ func (v *Vas) CreateOrder(ctx *gin.Context, req *vasproto.CreateOrderReq) (data
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Oid3 = wxpayCli.AppId
|
req.Oid3 = wxpayCli.AppId
|
||||||
|
case vasproto.PayTypeCoin:
|
||||||
|
return v.CreateOrderByCoin(ctx, req, product)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加订单
|
// 添加订单
|
||||||
|
@ -344,6 +346,110 @@ func (v *Vas) CreateOrder(ctx *gin.Context, req *vasproto.CreateOrderReq) (data
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Vas) CreateOrderByCoin(ctx *gin.Context, req *vasproto.CreateOrderReq, product *dbstruct.Product) (data *vasproto.CreateOrderData, err error) {
|
||||||
|
var (
|
||||||
|
order *dbstruct.Order
|
||||||
|
orderId = idgenerator.GenOrderId() // 订单id
|
||||||
|
priceCoin = product.GetRealPriceCoin()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx, err := v.store.VasBegin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("vas begin fail, err: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("global err, req: %v, order: %v, err: %v", util.ToJson(req), util.ToJson(order), err)
|
||||||
|
}
|
||||||
|
errTx := v.store.DealTxCR(tx, err)
|
||||||
|
if errTx != nil {
|
||||||
|
logger.Error("DealTxCR fail, err: %v", errTx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 检查钱包
|
||||||
|
wallet, exists := v.CheckWalletExist(ctx, tx, req.Mid)
|
||||||
|
if !exists {
|
||||||
|
err = errs.ErrVasWalletNotExist
|
||||||
|
logger.Error("CheckWalletExist fail, mid: %v, err: %v", req.Mid, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if wallet.GetCoins() <= priceCoin {
|
||||||
|
err = errs.ErrVasNoEnoughCoin
|
||||||
|
logger.Error("not enough coin, mid: %v, coins: %v, err: %v", req.Mid, wallet.Coins, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣金币
|
||||||
|
err = v.store.DecCoins(ctx, tx, req.Mid, priceCoin)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("DecCoins fail, mid: %v, priceCoin: %v, err: %v", req.Mid, priceCoin, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加订单
|
||||||
|
var (
|
||||||
|
timeNow = time.Now().Unix()
|
||||||
|
)
|
||||||
|
order = &dbstruct.Order{
|
||||||
|
ID: goproto.String(orderId),
|
||||||
|
Mid: goproto.Int64(req.Mid),
|
||||||
|
Uid: goproto.Int64(req.Uid),
|
||||||
|
Oid1: goproto.String(req.Oid1),
|
||||||
|
Oid2: goproto.String(req.Oid2),
|
||||||
|
Oid3: goproto.String(req.Oid3),
|
||||||
|
ProductId: goproto.String(req.ProductId),
|
||||||
|
PayType: goproto.String(req.PayType),
|
||||||
|
PayAmount: goproto.Int64(product.RealPrice),
|
||||||
|
Coins: goproto.Int64(product.ValueCoins),
|
||||||
|
OrderStatus: goproto.Int32(dbstruct.VasOrderStatusInit),
|
||||||
|
OrderFrom: goproto.String(req.From),
|
||||||
|
Ct: goproto.Int64(timeNow),
|
||||||
|
Ut: goproto.Int64(timeNow),
|
||||||
|
Operator: goproto.String(req.Operator),
|
||||||
|
Did: goproto.String(req.Did),
|
||||||
|
Version: goproto.String(req.Version),
|
||||||
|
OsVersion: goproto.String(req.Version),
|
||||||
|
DevType: goproto.Int32(req.DevType),
|
||||||
|
Channel: goproto.String(req.Channel),
|
||||||
|
Model: goproto.String(req.Model),
|
||||||
|
NetType: goproto.String(req.NetType),
|
||||||
|
Ip: goproto.String(req.Ip),
|
||||||
|
}
|
||||||
|
err = v.store.CreateOrder(ctx, tx, order)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("CreateOrder fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
errTx := v.store.DealTxCR(tx, err)
|
||||||
|
if errTx != nil {
|
||||||
|
logger.Error("DealTxCR fail, err: %v", errTx)
|
||||||
|
err = errTx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动回调
|
||||||
|
err = v.PayCallback(ctx, &vasproto.PayCallbackParamIn{
|
||||||
|
OrderId: orderId,
|
||||||
|
OutOrderId: fmt.Sprintf("%s_bycoin", orderId),
|
||||||
|
CallbackPayType: vasproto.CallBackPayTypeCoin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("PayCallback fail, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = &vasproto.CreateOrderData{
|
||||||
|
OrderId: orderId,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Vas) CheckWalletExist(ctx *gin.Context, tx *sqlx.Tx, mid int64) (wallet *dbstruct.Wallet, exist bool) {
|
func (v *Vas) CheckWalletExist(ctx *gin.Context, tx *sqlx.Tx, mid int64) (wallet *dbstruct.Wallet, exist bool) {
|
||||||
wallet, err := v.store.GetWalletByMid(ctx, tx, mid)
|
wallet, err := v.store.GetWalletByMid(ctx, tx, mid)
|
||||||
switch err {
|
switch err {
|
||||||
|
@ -1249,7 +1355,7 @@ func (v *Vas) GetUserWechatUnlock(ctx *gin.Context, mid, uid int64) (uu *dbstruc
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【支付】回调
|
// 【支付】回调
|
||||||
func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) (err error) {
|
||||||
var (
|
var (
|
||||||
orderId = p.OrderId
|
orderId = p.OrderId
|
||||||
outOrderId = p.OutOrderId
|
outOrderId = p.OutOrderId
|
||||||
|
@ -1261,16 +1367,16 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
checkOrder, err := v.store.GetOrderById(ctx, nil, orderId)
|
checkOrder, err := v.store.GetOrderById(ctx, nil, orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("GetOrderById fail, p: %v, err: %v", util.ToJson(p), err)
|
logger.Error("GetOrderById fail, p: %v, err: %v", util.ToJson(p), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if checkOrder == nil {
|
if checkOrder == nil {
|
||||||
logger.Warn("GetOrderById nil, p: %v", util.ToJson(p))
|
logger.Warn("GetOrderById nil, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
// 是否已处理过的订单
|
// 是否已处理过的订单
|
||||||
if checkOrder.GetOrderStatus() != dbstruct.VasOrderStatusInit {
|
if checkOrder.GetOrderStatus() != dbstruct.VasOrderStatusInit {
|
||||||
logger.Error("repeat deal, p: %v", util.ToJson(p))
|
logger.Error("repeat deal, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// out_order_id检查
|
// out_order_id检查
|
||||||
|
@ -1281,29 +1387,29 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("GetOrderByOutOrderId fail, p: %v, err: %v", util.ToJson(p), err)
|
logger.Error("GetOrderByOutOrderId fail, p: %v, err: %v", util.ToJson(p), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if outOrder != nil {
|
if outOrder != nil {
|
||||||
logger.Error("out order exists, p: %v", util.ToJson(p))
|
logger.Error("out order exists, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取商品
|
// 获取商品
|
||||||
product, err := v.store.GetProductById(ctx, checkOrder.GetProductId())
|
product, err := v.store.GetProductById(ctx, checkOrder.GetProductId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("GetProductById fail, id: %v, err: %v", checkOrder.GetProductId(), err)
|
logger.Error("GetProductById fail, id: %v, err: %v", checkOrder.GetProductId(), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if product == nil {
|
if product == nil {
|
||||||
logger.Error("GetProductById nil, id: %v", checkOrder.GetProductId())
|
logger.Error("GetProductById nil, id: %v", checkOrder.GetProductId())
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 钱包
|
// 钱包
|
||||||
_, hasWallet := v.CheckWalletExist(ctx, nil, checkOrder.GetMid())
|
_, hasWallet := v.CheckWalletExist(ctx, nil, checkOrder.GetMid())
|
||||||
if !hasWallet {
|
if !hasWallet {
|
||||||
logger.Error("CheckWalletExist fail, mid: %v", checkOrder.GetMid())
|
logger.Error("CheckWalletExist fail, mid: %v", checkOrder.GetMid())
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解锁信息
|
// 解锁信息
|
||||||
|
@ -1313,7 +1419,7 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
_, hasZoneUnlock := v.CheckZoneUnlockExist(ctx, nil, mid, zid)
|
_, hasZoneUnlock := v.CheckZoneUnlockExist(ctx, nil, mid, zid)
|
||||||
if !hasZoneUnlock {
|
if !hasZoneUnlock {
|
||||||
logger.Error("CheckZoneUnlockExist fail, mid: %v, zid: %v", mid, zid)
|
logger.Error("CheckZoneUnlockExist fail, mid: %v, zid: %v", mid, zid)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1321,7 +1427,7 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
tx, err := v.store.VasBegin(ctx)
|
tx, err := v.store.VasBegin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("vas begin fail, err: %v", err)
|
logger.Error("vas begin fail, err: %v", err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = v.AddOplogOrder(
|
_ = v.AddOplogOrder(
|
||||||
|
@ -1368,7 +1474,7 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
order, err := v.store.GetOrderByIdForUpdate(ctx, tx, orderId)
|
order, err := v.store.GetOrderByIdForUpdate(ctx, tx, orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("GetOrderByIdForUpdate fail, p: %v", util.ToJson(p))
|
logger.Error("GetOrderByIdForUpdate fail, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 锁住钱包
|
// 锁住钱包
|
||||||
|
@ -1403,14 +1509,14 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
err = v.store.CreateConsumeHistory(ctx, tx, ch)
|
err = v.store.CreateConsumeHistory(ctx, tx, ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("CreateConsumeHistory fail, ch: %v, err: %v", util.ToJson(ch), err)
|
logger.Error("CreateConsumeHistory fail, ch: %v, err: %v", util.ToJson(ch), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusInit, dbstruct.VasOrderStatusPaySuccess)
|
err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusInit, dbstruct.VasOrderStatusPaySuccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UpdateOrderStatus fail, p: %v", util.ToJson(p))
|
logger.Error("UpdateOrderStatus fail, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
afterStatus = dbstruct.VasOrderStatusPaySuccess
|
afterStatus = dbstruct.VasOrderStatusPaySuccess
|
||||||
|
|
||||||
|
@ -1418,7 +1524,7 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
err = v.store.UpdateOutOrderId(ctx, tx, orderId, outOrderId)
|
err = v.store.UpdateOutOrderId(ctx, tx, orderId, outOrderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UpdateOutOrderId fail, p: %v", util.ToJson(p))
|
logger.Error("UpdateOutOrderId fail, p: %v", util.ToJson(p))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 商品判断
|
// 商品判断
|
||||||
|
@ -1428,12 +1534,12 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins())
|
err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("IncCoins fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("IncCoins fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusPaySuccess, dbstruct.VasOrderStatusFinish)
|
err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusPaySuccess, dbstruct.VasOrderStatusFinish)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UpdateOrderStatus fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("UpdateOrderStatus fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
afterStatus = dbstruct.VasOrderStatusFinish
|
afterStatus = dbstruct.VasOrderStatusFinish
|
||||||
case product.Id == dbstruct.ProductIdH5ContactWechat:
|
case product.Id == dbstruct.ProductIdH5ContactWechat:
|
||||||
|
@ -1441,32 +1547,32 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins())
|
err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("IncCoins fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("IncCoins fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
case product.Id == dbstruct.ProductIdMembership:
|
case product.Id == dbstruct.ProductIdMembership:
|
||||||
// 解锁会员资格
|
// 解锁会员资格
|
||||||
_, err = v.UnlockMembership(ctx, tx, util.DerefInt64(order.Mid), product, order, wallet)
|
_, err = v.UnlockMembership(ctx, tx, util.DerefInt64(order.Mid), product, order, wallet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UnlockMembership fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("UnlockMembership fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
case product.Id == dbstruct.ProductIdH5ZoneMoment:
|
case product.Id == dbstruct.ProductIdH5ZoneMoment:
|
||||||
err = v.UnlockZoneMoment(ctx, tx, order)
|
err = v.UnlockZoneMoment(ctx, tx, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UnlockZoneMoment fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("UnlockZoneMoment fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
case product.Id == dbstruct.ProductIdH5ZoneAdmission:
|
case product.Id == dbstruct.ProductIdH5ZoneAdmission:
|
||||||
err = v.UnlockZoneAdmission(ctx, tx, order, dbstruct.ZoneUnlockTypePay)
|
err = v.UnlockZoneAdmission(ctx, tx, order, dbstruct.ZoneUnlockTypePay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UnlockZoneAdmission fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("UnlockZoneAdmission fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
case product.Id == dbstruct.ProductIdH5ZoneSuperfanship:
|
case product.Id == dbstruct.ProductIdH5ZoneSuperfanship:
|
||||||
err = v.UnlockZoneSuperfanship(ctx, tx, order, dbstruct.ZoneUnlockTypePay)
|
err = v.UnlockZoneSuperfanship(ctx, tx, order, dbstruct.ZoneUnlockTypePay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("UnlockZoneSuperfanship fail, order: %v, err: %v", util.ToJson(order), err)
|
logger.Error("UnlockZoneSuperfanship fail, order: %v, err: %v", util.ToJson(order), err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1474,8 +1580,9 @@ func (v *Vas) PayCallback(ctx *gin.Context, p *vasproto.PayCallbackParamIn) {
|
||||||
_err := v.UnlockZoneIronfanshipReachConsume(ctx, tx, order.GetMid(), order.GetZid(), order.GetUid())
|
_err := v.UnlockZoneIronfanshipReachConsume(ctx, tx, order.GetMid(), order.GetZid(), order.GetUid())
|
||||||
if _err != nil {
|
if _err != nil {
|
||||||
logger.Error("UnlockZoneIronfanshipReachConsume fail, order: %v, err: %v", util.ToJson(order), _err)
|
logger.Error("UnlockZoneIronfanshipReachConsume fail, order: %v, err: %v", util.ToJson(order), _err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vas) GetCoinOrderById(ctx *gin.Context, id string) (*dbstruct.CoinOrder, error) {
|
func (v *Vas) GetCoinOrderById(ctx *gin.Context, id string) (*dbstruct.CoinOrder, error) {
|
||||||
|
|
|
@ -499,7 +499,7 @@ func (s *Service) H5DirectUnlockWechat(ctx *gin.Context, req *vasproto.H5DirectU
|
||||||
|
|
||||||
// 支付回调
|
// 支付回调
|
||||||
func (s *Service) PayCallback(ctx *gin.Context, req *vasproto.PayCallbackParamIn) (data *vasproto.H5DirectUnlockWechatData, ec errcode.ErrCode) {
|
func (s *Service) PayCallback(ctx *gin.Context, req *vasproto.PayCallbackParamIn) (data *vasproto.H5DirectUnlockWechatData, ec errcode.ErrCode) {
|
||||||
_DefaultVas.PayCallback(ctx, req)
|
_ = _DefaultVas.PayCallback(ctx, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,3 +96,7 @@ type Product struct {
|
||||||
Images []*ToCImage `json:"images"`
|
Images []*ToCImage `json:"images"`
|
||||||
Videos []*ToCVideo `json:"videos"`
|
Videos []*ToCVideo `json:"videos"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Product) GetRealPriceCoin() int64 {
|
||||||
|
return p.RealPrice / 10
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue