321 lines
10 KiB
Go
321 lines
10 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/ospgateway"
|
|
)
|
|
|
|
// 账单能力封装 OSP Gateway(发票列表/明细/PDF/付款与订阅付款方式)。
|
|
// 该服务只在租户主区域提供,所有请求都要携带 ospHomeRegion 并发往主区域。
|
|
|
|
// Invoice 是一张发票的摘要(InvoiceSummary 的面板投影)。
|
|
type Invoice struct {
|
|
ID string `json:"id"`
|
|
InternalID string `json:"internalId"`
|
|
Number string `json:"number"`
|
|
Type string `json:"type"`
|
|
Status string `json:"status"`
|
|
IsPaid bool `json:"isPaid"`
|
|
IsPayable bool `json:"isPayable"`
|
|
IsPaymentFailed bool `json:"isPaymentFailed"`
|
|
Amount float64 `json:"amount"`
|
|
AmountDue float64 `json:"amountDue"`
|
|
Currency string `json:"currency"`
|
|
TimeInvoice *time.Time `json:"timeInvoice"`
|
|
TimeDue *time.Time `json:"timeDue"`
|
|
}
|
|
|
|
// InvoiceLine 是发票内一行费用明细。
|
|
type InvoiceLine struct {
|
|
Product string `json:"product"`
|
|
OrderNo string `json:"orderNo"`
|
|
Quantity float64 `json:"quantity"`
|
|
UnitPrice float64 `json:"unitPrice"`
|
|
Total float64 `json:"total"`
|
|
Currency string `json:"currency"`
|
|
TimeStart *time.Time `json:"timeStart"`
|
|
TimeEnd *time.Time `json:"timeEnd"`
|
|
}
|
|
|
|
// PaymentMethod 是订阅上登记的一种付款方式,按 method 分信用卡/PayPal 两形态。
|
|
type PaymentMethod struct {
|
|
Method string `json:"method"` // CREDIT_CARD / PAYPAL
|
|
CardType string `json:"cardType,omitempty"`
|
|
LastDigits string `json:"lastDigits,omitempty"`
|
|
NameOnCard string `json:"nameOnCard,omitempty"`
|
|
TimeExpiration *time.Time `json:"timeExpiration,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
PayerName string `json:"payerName,omitempty"`
|
|
BillingAgreement string `json:"billingAgreement,omitempty"`
|
|
}
|
|
|
|
// ospHomeRegion 解析租户主区域公共名(经测活拿三字码再查区域表);
|
|
// 主区域是租户常量,按 tenancy 进程内缓存,免去每次账单调用先付一次远程测活。
|
|
func (c *RealClient) ospHomeRegion(ctx context.Context, cred Credentials) (string, error) {
|
|
if v, ok := c.homeRegions.Load(cred.TenancyOCID); ok {
|
|
return v.(string), nil
|
|
}
|
|
info, err := c.ValidateKey(ctx, cred)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve osp home region: %w", err)
|
|
}
|
|
r, ok := RegionByKey(info.HomeRegionKey)
|
|
if !ok {
|
|
return "", fmt.Errorf("resolve osp home region: unknown region key %q", info.HomeRegionKey)
|
|
}
|
|
if cred.TenancyOCID != "" {
|
|
c.homeRegions.Store(cred.TenancyOCID, r.Name)
|
|
}
|
|
return r.Name, nil
|
|
}
|
|
|
|
// ospInvoiceClient 构造发票服务客户端并指向主区域;region 同时用于请求参数。
|
|
func (c *RealClient) ospInvoiceClient(ctx context.Context, cred Credentials) (ospgateway.InvoiceServiceClient, string, error) {
|
|
region, err := c.ospHomeRegion(ctx, cred)
|
|
if err != nil {
|
|
return ospgateway.InvoiceServiceClient{}, "", err
|
|
}
|
|
ic, err := ospgateway.NewInvoiceServiceClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return ospgateway.InvoiceServiceClient{}, "", fmt.Errorf("new invoice client: %w", err)
|
|
}
|
|
applyProxy(&ic.BaseClient, cred)
|
|
ic.SetRegion(region)
|
|
return ic, region, nil
|
|
}
|
|
|
|
// ListInvoices 实现 Client:分页列出租户发票;year>0 时只取该自然年
|
|
// (按 timeInvoice 窗口过滤),为 0 拉全量。
|
|
func (c *RealClient) ListInvoices(ctx context.Context, cred Credentials, year int) ([]Invoice, error) {
|
|
ic, region, err := c.ospInvoiceClient(ctx, cred)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := ospgateway.ListInvoicesRequest{
|
|
OspHomeRegion: ®ion,
|
|
CompartmentId: &cred.TenancyOCID,
|
|
Limit: common.Int(ospPageLimit),
|
|
}
|
|
if year > 0 {
|
|
req.TimeInvoiceStart = &common.SDKTime{Time: time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)}
|
|
req.TimeInvoiceEnd = &common.SDKTime{Time: time.Date(year+1, 1, 1, 0, 0, 0, 0, time.UTC)}
|
|
}
|
|
var out []Invoice
|
|
for {
|
|
resp, err := ic.ListInvoices(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list invoices: %w", err)
|
|
}
|
|
for _, item := range resp.Items {
|
|
out = append(out, toInvoice(item))
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return orEmpty(out), nil
|
|
}
|
|
req.Page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
// ospPageLimit 是 OSP 网关列表接口的每页条数;不传时服务端默认页极小,
|
|
// 几百行发票明细要串行翻几十页(实测单请求被拖到 40s+)。
|
|
const ospPageLimit = 100
|
|
|
|
// ListInvoiceLines 实现 Client:分页列出一张发票的全部费用行。
|
|
func (c *RealClient) ListInvoiceLines(ctx context.Context, cred Credentials, internalInvoiceID string) ([]InvoiceLine, error) {
|
|
ic, region, err := c.ospInvoiceClient(ctx, cred)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []InvoiceLine
|
|
var page *string
|
|
for {
|
|
resp, err := ic.ListInvoiceLines(ctx, ospgateway.ListInvoiceLinesRequest{
|
|
OspHomeRegion: ®ion,
|
|
CompartmentId: &cred.TenancyOCID,
|
|
InternalInvoiceId: &internalInvoiceID,
|
|
Page: page,
|
|
Limit: common.Int(ospPageLimit),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list invoice lines: %w", err)
|
|
}
|
|
for _, item := range resp.Items {
|
|
out = append(out, toInvoiceLine(item))
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return orEmpty(out), nil
|
|
}
|
|
page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
// DownloadInvoicePdf 实现 Client:整读发票 PDF,超出 maxBytes 视为异常。
|
|
func (c *RealClient) DownloadInvoicePdf(ctx context.Context, cred Credentials, internalInvoiceID string, maxBytes int64) ([]byte, error) {
|
|
ic, region, err := c.ospInvoiceClient(ctx, cred)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := ic.DownloadPdfContent(ctx, ospgateway.DownloadPdfContentRequest{
|
|
OspHomeRegion: ®ion,
|
|
CompartmentId: &cred.TenancyOCID,
|
|
InternalInvoiceId: &internalInvoiceID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download invoice pdf: %w", err)
|
|
}
|
|
defer resp.Content.Close()
|
|
data, err := io.ReadAll(io.LimitReader(resp.Content, maxBytes+1))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download invoice pdf: %w", err)
|
|
}
|
|
if int64(len(data)) > maxBytes {
|
|
return nil, fmt.Errorf("download invoice pdf: exceeds %d bytes", maxBytes)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// PayInvoice 实现 Client:按订阅当前默认付款方式支付发票,email 接收回执。
|
|
func (c *RealClient) PayInvoice(ctx context.Context, cred Credentials, internalInvoiceID, email string) error {
|
|
ic, region, err := c.ospInvoiceClient(ctx, cred)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = ic.PayInvoice(ctx, ospgateway.PayInvoiceRequest{
|
|
OspHomeRegion: ®ion,
|
|
CompartmentId: &cred.TenancyOCID,
|
|
InternalInvoiceId: &internalInvoiceID,
|
|
PayInvoiceDetails: ospgateway.PayInvoiceDetails{Email: &email},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("pay invoice: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListPaymentMethods 实现 Client:汇总各订阅上登记的全部付款方式。
|
|
func (c *RealClient) ListPaymentMethods(ctx context.Context, cred Credentials) ([]PaymentMethod, error) {
|
|
region, err := c.ospHomeRegion(ctx, cred)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sc, err := ospgateway.NewSubscriptionServiceClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new osp subscription client: %w", err)
|
|
}
|
|
applyProxy(&sc.BaseClient, cred)
|
|
sc.SetRegion(region)
|
|
var out []PaymentMethod
|
|
var page *string
|
|
for {
|
|
resp, err := sc.ListSubscriptions(ctx, ospgateway.ListSubscriptionsRequest{
|
|
OspHomeRegion: ®ion,
|
|
CompartmentId: &cred.TenancyOCID,
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list payment methods: %w", err)
|
|
}
|
|
for _, sub := range resp.Items {
|
|
out = appendPaymentMethods(out, sub.PaymentOptions)
|
|
}
|
|
if resp.OpcNextPage == nil {
|
|
return orEmpty(out), nil
|
|
}
|
|
page = resp.OpcNextPage
|
|
}
|
|
}
|
|
|
|
func toInvoice(v ospgateway.InvoiceSummary) Invoice {
|
|
inv := Invoice{
|
|
ID: deref(v.InvoiceId),
|
|
InternalID: deref(v.InternalInvoiceId),
|
|
Number: deref(v.InvoiceNumber),
|
|
Type: string(v.InvoiceType),
|
|
Status: string(v.InvoiceStatus),
|
|
Amount: money(v.InvoiceAmount),
|
|
AmountDue: money(v.InvoiceAmountDue),
|
|
TimeInvoice: sdkTime(v.TimeInvoice),
|
|
TimeDue: sdkTime(v.TimeInvoiceDue),
|
|
}
|
|
if v.IsPaid != nil {
|
|
inv.IsPaid = *v.IsPaid
|
|
}
|
|
if v.IsPayable != nil {
|
|
inv.IsPayable = *v.IsPayable
|
|
}
|
|
if v.IsPaymentFailed != nil {
|
|
inv.IsPaymentFailed = *v.IsPaymentFailed
|
|
}
|
|
if v.Currency != nil {
|
|
inv.Currency = deref(v.Currency.CurrencyCode)
|
|
}
|
|
return inv
|
|
}
|
|
|
|
func toInvoiceLine(v ospgateway.InvoiceLineSummary) InvoiceLine {
|
|
line := InvoiceLine{
|
|
Product: deref(v.Product),
|
|
OrderNo: deref(v.OrderNo),
|
|
Quantity: money(v.Quantity),
|
|
UnitPrice: money(v.NetUnitPrice),
|
|
Total: money(v.TotalPrice),
|
|
TimeStart: sdkTime(v.TimeStart),
|
|
TimeEnd: sdkTime(v.TimeEnd),
|
|
}
|
|
if v.Currency != nil {
|
|
line.Currency = deref(v.Currency.CurrencyCode)
|
|
}
|
|
return line
|
|
}
|
|
|
|
func appendPaymentMethods(out []PaymentMethod, opts []ospgateway.PaymentOption) []PaymentMethod {
|
|
for _, opt := range opts {
|
|
switch v := opt.(type) {
|
|
case ospgateway.CreditCardPaymentOption:
|
|
out = append(out, creditCardMethod(v))
|
|
case *ospgateway.CreditCardPaymentOption:
|
|
out = append(out, creditCardMethod(*v))
|
|
case ospgateway.PaypalPaymentOption:
|
|
out = append(out, paypalMethod(v))
|
|
case *ospgateway.PaypalPaymentOption:
|
|
out = append(out, paypalMethod(*v))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func creditCardMethod(v ospgateway.CreditCardPaymentOption) PaymentMethod {
|
|
return PaymentMethod{
|
|
Method: "CREDIT_CARD",
|
|
CardType: string(v.CreditCardType),
|
|
LastDigits: deref(v.LastDigits),
|
|
NameOnCard: deref(v.NameOnCard),
|
|
TimeExpiration: sdkTime(v.TimeExpiration),
|
|
}
|
|
}
|
|
|
|
func paypalMethod(v ospgateway.PaypalPaymentOption) PaymentMethod {
|
|
return PaymentMethod{
|
|
Method: "PAYPAL",
|
|
Email: deref(v.EmailAddress),
|
|
PayerName: strings.TrimSpace(deref(v.FirstName) + " " + deref(v.LastName)),
|
|
BillingAgreement: deref(v.ExtBillingAgreementId),
|
|
}
|
|
}
|
|
|
|
// money 把 SDK 的 float32 金额转为 float64 输出;四舍五入到 4 位小数,
|
|
// 抹掉精度转换产生的二进制噪音(如 603.53 变 603.5300293)。
|
|
func money(p *float32) float64 {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return math.Round(float64(*p)*10000) / 10000
|
|
}
|