diff --git a/service/invoice.go b/service/invoice.go new file mode 100644 index 0000000..59b4716 --- /dev/null +++ b/service/invoice.go @@ -0,0 +1,180 @@ +package service + +import ( + "electricity_bill_calc/exceptions" + "electricity_bill_calc/global" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/repository" + "electricity_bill_calc/types" + + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/postgres" + "github.com/samber/lo" + "github.com/shopspring/decimal" + "go.uber.org/zap" +) + +type _InvoiceSerivce struct { + log *zap.Logger + ds goqu.DialectWrapper +} + +var InvoiceService = _InvoiceSerivce{ + log: logger.Named("Service", "Invoice"), + ds: goqu.Dialect("postgres"), +} + +// 获取指定的发票信息,包括发票覆盖的商户核算信息 +func (is _InvoiceSerivce) GetInvoice(invoiceNo string) (*model.Invoice, []*model.SimplifiedTenementCharge, error) { + is.log.Info("获取指定发票的信息", zap.String("InvoiceNo", invoiceNo)) + invoice, err := repository.InvoiceRepository.GetInvoiceDetail(invoiceNo) + if err != nil || invoice == nil { + is.log.Error("获取指定发票的信息失败", zap.Error(err)) + return nil, nil, exceptions.NewNotFoundError("指定发票信息不存在。") + } + + charges, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(invoice.Tenement, invoice.Covers) + if err != nil { + is.log.Error("获取指定发票的信息失败", zap.Error(err)) + return nil, nil, err + } + return invoice, charges, nil +} + +// 根据给定的商户核算记录和发票基本信息,计算发票中的货物信息 +func (is _InvoiceSerivce) CalculateInvoiceAmount(method int16, rate decimal.Decimal, reports []*model.SimplifiedTenementCharge) (decimal.Decimal, []*model.InvoiceCargo, error) { + is.log.Info("计算指定商户发票中的货物信息", zap.Int16("Method", method), logger.DecimalField("Rate", rate)) + tenementConsumptionTotal := lo.Reduce(reports, func(agg decimal.Decimal, r *model.SimplifiedTenementCharge, _ int) decimal.Decimal { + return agg.Add(r.TotalConsumption) + }, decimal.Zero) + tenementChargeTotal := lo.Reduce(reports, func(agg decimal.Decimal, r *model.SimplifiedTenementCharge, _ int) decimal.Decimal { + return agg.Add(r.FinalCharge) + }, decimal.Zero) + if tenementConsumptionTotal.IsZero() { + err := exceptions.NewInsufficientDataError("TotalConsumption", "商户核算记录中没有电量消耗数据。") + is.log.Warn("计算指定商户发票中的货物信息失败", zap.Error(err)) + return decimal.Zero, nil, err + } + var tenementTaxTotal, chargePrice, cargoTotal decimal.Decimal + switch method { + case model.TAX_METHOD_INCLUSIVE: + tenementTaxTotal = tenementChargeTotal.Div(rate.Add(decimal.NewFromInt(1))).Mul(rate) + chargePrice = (tenementChargeTotal.Sub(tenementTaxTotal)).Div(tenementConsumptionTotal) + cargoTotal = tenementChargeTotal + case model.TAX_METHOD_EXCLUSIVE: + tenementTaxTotal = tenementChargeTotal.Mul(rate) + chargePrice = tenementChargeTotal.Div(tenementConsumptionTotal) + cargoTotal = tenementChargeTotal.Add(tenementTaxTotal) + default: + return decimal.Zero, make([]*model.InvoiceCargo, 0), exceptions.NewIllegalArgumentsError("不支持的税率计算方式。") + } + cargos := []*model.InvoiceCargo{ + { + Name: "电费", + Unit: "千瓦时", + Quantity: tenementConsumptionTotal, + Price: chargePrice.RoundBank(2), + Total: tenementChargeTotal.RoundBank(2), + TaxRate: rate.RoundBank(2), + Tax: tenementTaxTotal.RoundBank(2), + }, + } + return cargoTotal.RoundBank(2), cargos, nil +} + +// 利用用户提供的内容对发票数据进行试计算 +func (is _InvoiceSerivce) TestCalculateInvoice(pid, tid string, method int16, rate decimal.NullDecimal, covers []string) (decimal.Decimal, []*model.InvoiceCargo, error) { + is.log.Info("试计算发票票面数据", zap.String("Park", pid), zap.String("Tenement", tid), zap.Int16("Method", method), logger.DecimalField("Rate", rate.Decimal)) + park, err := repository.ParkRepository.RetrieveParkDetail(pid) + if err != nil || park == nil { + is.log.Error("试计算发票票面数据失败,未能获取到指定园区的信息", zap.Error(err)) + return decimal.Zero, nil, exceptions.NewNotFoundError("指定的园区不存在。") + } + if !rate.Valid && !park.TaxRate.Valid { + is.log.Error("试计算发票票面数据失败,必须要设定发票税率") + return decimal.Zero, nil, exceptions.NewIllegalArgumentsError("必须要设定发票税率。") + } + taxRate := park.TaxRate.Decimal + if rate.Valid { + taxRate = rate.Decimal + } + reports, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(tid, covers) + if err != nil { + is.log.Error("试计算发票票面数据失败,未能获取到指定商户的核算记录", zap.Error(err)) + return decimal.Zero, nil, err + } + return is.CalculateInvoiceAmount(method, taxRate, reports) +} + +// 记录一个新的发票信息 +func (is _InvoiceSerivce) SaveInvoice(pid, tid, invoiceNo string, invoiceType *string, method int16, rate decimal.NullDecimal, covers []string) error { + is.log.Info("记录一个新的发票信息", zap.String("Park", pid), zap.String("Tenement", tid), zap.String("InvoiceNo", invoiceNo)) + park, err := repository.ParkRepository.RetrieveParkDetail(pid) + if err != nil || park == nil { + is.log.Error("记录一个新的发票信息失败,未能获取到指定园区的信息", zap.Error(err)) + return exceptions.NewNotFoundError("指定的园区不存在。") + } + if !rate.Valid && park.TaxRate.Valid { + is.log.Error("记录一个新的发票信息失败,必须要设定发票税率") + return exceptions.NewIllegalArgumentsError("必须要设定发票税率。") + } + taxRate := park.TaxRate.Decimal + if rate.Valid { + taxRate = rate.Decimal + } + reports, err := repository.InvoiceRepository.GetSimplifiedTenementCharges(tid, covers) + if err != nil { + is.log.Error("记录一个新的发票信息失败,未能获取到指定商户的核算记录", zap.Error(err)) + return exceptions.NewUnsuccessQueryError("未能获取到指定商户的核算记录。") + } + total, cargos, err := is.CalculateInvoiceAmount(method, taxRate, reports) + if err != nil { + is.log.Error("记录一个新的发票信息失败,未能计算发票票面数据", zap.Error(err)) + return exceptions.NewUnsuccessCalculateError("未能计算发票票面数据。") + } + issuedAt := types.Now() + + err = repository.InvoiceRepository.Create(pid, tid, invoiceNo, invoiceType, total, issuedAt, method, taxRate, &cargos, &covers) + if err != nil { + is.log.Error("记录一个新的发票信息失败,未能保存发票信息", zap.Error(err)) + return exceptions.NewUnsuccessCreateError("未能保存发票信息。") + } + return nil +} + +// 删除指定的发票信息 +func (is _InvoiceSerivce) DeleteInvoice(invoiceNo string) error { + is.log.Info("删除指定的发票信息", zap.String("InvoiceNo", invoiceNo)) + invoice, err := repository.InvoiceRepository.GetInvoiceDetail(invoiceNo) + if err != nil || invoice == nil { + is.log.Error("删除指定的发票信息失败,未能获取到指定发票的信息", zap.Error(err)) + return exceptions.NewNotFoundError("指定的发票信息不存在。") + } + ctx, cancel := global.TimeoutContext() + defer cancel() + tx, err := global.DB.Begin(ctx) + if err != nil { + is.log.Error("删除指定的发票信息失败,未能开启事务", zap.Error(err)) + return exceptions.NewUnsuccessDBTransactionError("未能开启事务。") + } + err = repository.InvoiceRepository.Delete(tx, ctx, invoiceNo) + if err != nil { + is.log.Error("删除指定的发票信息失败,未能删除发票信息", zap.Error(err)) + tx.Rollback(ctx) + return exceptions.NewUnsuccessDeleteError("未能删除发票信息。") + } + err = repository.InvoiceRepository.DeleteInvoiceTenementRelation(tx, ctx, invoiceNo) + if err != nil { + is.log.Error("删除指定的发票信息失败,未能删除发票与商户核算记录之间的关联", zap.Error(err)) + tx.Rollback(ctx) + return exceptions.NewUnsuccessDeleteError("未能删除发票与商户核算记录之间的关联。") + } + err = tx.Commit(ctx) + if err != nil { + is.log.Error("删除指定的发票信息失败,未能提交事务", zap.Error(err)) + tx.Rollback(ctx) + return exceptions.NewUnsuccessDBTransactionError("未能提交事务。") + } + return nil +}