Payment Flow + Access Flow Change
This commit is contained in:
@@ -17,6 +17,11 @@ import (
|
||||
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
|
||||
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
|
||||
@@ -67,6 +72,11 @@ const (
|
||||
ALLOWED_IMAGE = tools.ALLOWED_IMAGE
|
||||
SERVICE_RESOURCE = tools.SERVICE_RESOURCE
|
||||
LIVE_SERVICE = tools.LIVE_SERVICE
|
||||
BILL = tools.BILL
|
||||
PAYMENT = tools.PAYMENT
|
||||
REFUND = tools.REFUND
|
||||
DISCOUNT = tools.DISCOUNT
|
||||
SUBSCRIPTION = tools.SUBSCRIPTION
|
||||
)
|
||||
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
|
||||
@@ -727,6 +737,41 @@ func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToBill() *billing.Bill {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.BILL {
|
||||
return l.Data.(*billing.Bill)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToPayment() *payment.Payment {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.PAYMENT {
|
||||
return l.Data.(*payment.Payment)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToRefund() *refund.Refund {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.REFUND {
|
||||
return l.Data.(*refund.Refund)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToDiscount() *discount.Discount {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.DISCOUNT {
|
||||
return l.Data.(*discount.Discount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToSubscription() *subscription.Subscription {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.SUBSCRIPTION {
|
||||
return l.Data.(*subscription.Subscription)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------- Loading resources ----------GetAccessor
|
||||
|
||||
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
package bill
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
/*
|
||||
* Booking is a struct that represents a booking
|
||||
*/
|
||||
|
||||
type Bill struct {
|
||||
utils.AbstractObject
|
||||
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
|
||||
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
|
||||
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
|
||||
Total float64 `json:"total" bson:"total" validate:"required"`
|
||||
}
|
||||
|
||||
func (ri *Bill) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "order":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.ORDER)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func GenerateBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
// hhmmm : should get... the loop.
|
||||
return &Bill{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
|
||||
IsDraft: false,
|
||||
},
|
||||
OrderID: order.UUID,
|
||||
Status: enum.PENDING,
|
||||
// SubOrders: peerOrders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
peers := map[string][]*PeerItemOrder{}
|
||||
for _, p := range order.Purchases {
|
||||
// TODO : if once
|
||||
if _, ok := peers[p.DestPeerID]; !ok {
|
||||
peers[p.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
|
||||
ResourceType: p.ResourceType,
|
||||
Purchase: p,
|
||||
Item: p.PricedItem,
|
||||
Quantity: 1,
|
||||
})
|
||||
}
|
||||
for _, b := range order.Bookings {
|
||||
// TODO : if once
|
||||
isPurchased := false
|
||||
for _, p := range order.Purchases {
|
||||
if p.ResourceID == b.ResourceID {
|
||||
isPurchased = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isPurchased {
|
||||
continue
|
||||
}
|
||||
if _, ok := peers[b.DestPeerID]; !ok {
|
||||
peers[b.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
|
||||
ResourceType: b.ResourceType,
|
||||
Quantity: 1,
|
||||
Item: b.PricedItem,
|
||||
})
|
||||
}
|
||||
peerOrders := map[string]*PeerOrder{}
|
||||
for peerID, items := range peers {
|
||||
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peerOrders[peerID] = &PeerOrder{
|
||||
PeerID: peerID,
|
||||
BillingAddress: pr.(*peer.Peer).WalletAddress,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
bill := &Bill{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
|
||||
IsDraft: true,
|
||||
},
|
||||
OrderID: order.UUID,
|
||||
Status: enum.PENDING,
|
||||
SubOrders: peerOrders,
|
||||
}
|
||||
return bill.SumUpBill(request)
|
||||
}
|
||||
|
||||
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
func (r *Bill) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
}
|
||||
|
||||
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
if !r.IsDraft && r.Status != set.(*Bill).Status {
|
||||
return true, &Bill{Status: set.(*Bill).Status} // only state can be updated
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Bill) CanDelete() bool {
|
||||
return r.IsDraft // only draft order can be deleted
|
||||
}
|
||||
|
||||
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
|
||||
for _, b := range d.SubOrders {
|
||||
err := b.SumUpBill(request)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
d.Total += b.Total
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
type PeerOrder struct {
|
||||
Error string `json:"error,omitempty" bson:"error,omitempty"`
|
||||
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
|
||||
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
|
||||
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
|
||||
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
|
||||
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
|
||||
}
|
||||
|
||||
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
|
||||
switch dt {
|
||||
case tools.PROCESSING_RESOURCE:
|
||||
return &resources.PricedProcessingResource{}
|
||||
case tools.STORAGE_RESOURCE:
|
||||
return &resources.PricedStorageResource{}
|
||||
case tools.DATA_RESOURCE:
|
||||
return &resources.PricedDataResource{}
|
||||
case tools.COMPUTE_RESOURCE:
|
||||
return &resources.PricedComputeResource{}
|
||||
case tools.WORKFLOW_RESOURCE:
|
||||
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
|
||||
|
||||
d.Status = enum.PENDING
|
||||
go func() {
|
||||
|
||||
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER ON RETURN UPDATE STATUS
|
||||
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
|
||||
if d.Status == enum.PAID {
|
||||
for _, b := range d.Items {
|
||||
priced := PricedByType(b.ResourceType)
|
||||
bb, _ := json.Marshal(b.Item)
|
||||
json.Unmarshal(bb, priced)
|
||||
if !priced.IsPurchasable() {
|
||||
continue
|
||||
}
|
||||
accessor := purchase_resource.NewAccessor(request)
|
||||
accessor.StoreOne(&purchase_resource.PurchaseResource{
|
||||
ResourceID: priced.GetID(),
|
||||
ResourceType: priced.GetType(),
|
||||
EndDate: priced.GetLocationEnd(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if d.Status != enum.PENDING {
|
||||
response <- d
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
|
||||
for _, b := range d.Items {
|
||||
tot, err := b.GetPriceHT(request) // missing something
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Total += tot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PeerItemOrder struct {
|
||||
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
|
||||
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
|
||||
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
|
||||
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
|
||||
}
|
||||
|
||||
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
|
||||
/////////// Temporary in order to allow GenerateOrder to complete while billing is still WIP
|
||||
if d.Purchase == nil {
|
||||
return 0, nil
|
||||
}
|
||||
///////////
|
||||
priced := PricedByType(d.ResourceType)
|
||||
b, _ := json.Marshal(d.Item)
|
||||
err := json.Unmarshal(b, priced)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
accessor := purchase_resource.NewAccessor(request)
|
||||
search, code, _ := accessor.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
|
||||
},
|
||||
}, "", d.Purchase.IsDraft, 0, 10000)
|
||||
if code == 200 && len(search) > 0 {
|
||||
for _, s := range search {
|
||||
if s.(*purchase_resource.PurchaseResource).EndDate == nil || time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
p, err := priced.GetPriceHT()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p * float64(d.Quantity), nil
|
||||
}
|
||||
|
||||
// WTF HOW TO SELECT THE RIGHT PRICE ???
|
||||
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED
|
||||
@@ -0,0 +1,421 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Bill struct {
|
||||
utils.AbstractObject
|
||||
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
|
||||
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
|
||||
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
|
||||
Total float64 `json:"total" bson:"total" validate:"required"`
|
||||
}
|
||||
|
||||
func (ri *Bill) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "order":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.ORDER)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
// IsFullySettled retourne vrai quand chaque ligne de chaque peer-order est réglée.
|
||||
func (b *Bill) IsFullySettled() bool {
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if !item.Settled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SettledTotal retourne le montant total des lignes déjà réglées.
|
||||
func (b *Bill) SettledTotal() float64 {
|
||||
total := 0.0
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if item.Settled {
|
||||
total += item.UnitPriceHT * float64(item.Quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// MarkItemSettled marque une ligne comme réglée d'après son itemID
|
||||
// et propage le statut PAID sur le PeerOrder si toutes ses lignes sont réglées.
|
||||
func (b *Bill) MarkItemSettled(itemID string) bool {
|
||||
now := time.Now().UTC()
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if item.ItemID == itemID {
|
||||
item.Settled = true
|
||||
item.SettledAt = &now
|
||||
// propage le statut PAID si toutes les lignes du peer sont réglées
|
||||
if po.IsFullySettled() {
|
||||
po.Status = enum.PAID
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GenerateBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
return &Bill{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
|
||||
IsDraft: false,
|
||||
},
|
||||
OrderID: ord.UUID,
|
||||
Status: enum.PENDING,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DraftFirstBill crée le premier brouillon de facture pour un order.
|
||||
// Règle :
|
||||
// - Calcul total indépendant du mode de paiement (photo du coût réel).
|
||||
// - Les purchases sont toujours BILL_ONCE / PAY_ONCE → réglées immédiatement.
|
||||
// - Les bookings avec BillingStrategy != BILL_ONCE génèrent des Subscription.
|
||||
// - Chaque ligne reçoit un ItemID unique pour le suivi de règlement.
|
||||
func DraftFirstBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
peers := map[string][]*PeerItemOrder{}
|
||||
|
||||
// Purchases : facturation immédiate, pas de subscription
|
||||
for _, p := range ord.Purchases {
|
||||
if _, ok := peers[p.DestPeerID]; !ok {
|
||||
peers[p.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
|
||||
ItemID: uuid.New().String(),
|
||||
ResourceType: p.ResourceType,
|
||||
Purchase: p,
|
||||
Item: p.PricedItem,
|
||||
Quantity: 1,
|
||||
BillingStrategy: pricing.BILL_ONCE,
|
||||
PaymentType: pricing.PAY_ONCE,
|
||||
})
|
||||
}
|
||||
|
||||
// Bookings : exclure les ressources déjà achetées (purchase_resource existant)
|
||||
purchasedIDs := map[string]bool{}
|
||||
for _, p := range ord.Purchases {
|
||||
purchasedIDs[p.ResourceID] = true
|
||||
}
|
||||
for _, b := range ord.Bookings {
|
||||
if purchasedIDs[b.ResourceID] {
|
||||
continue
|
||||
}
|
||||
if _, ok := peers[b.DestPeerID]; !ok {
|
||||
peers[b.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
|
||||
ItemID: uuid.New().String(),
|
||||
ResourceType: b.ResourceType,
|
||||
Quantity: 1,
|
||||
Item: b.PricedItem,
|
||||
BillingStrategy: b.BillingStrategy,
|
||||
PaymentType: b.PaymentType,
|
||||
})
|
||||
}
|
||||
|
||||
// Résolution des adresses de facturation peer
|
||||
peerOrders := map[string]*PeerOrder{}
|
||||
for peerID, items := range peers {
|
||||
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peerOrders[peerID] = &PeerOrder{
|
||||
PeerID: peerID,
|
||||
BillingAddress: pr.(*peer.Peer).WalletAddress,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
bill := &Bill{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
|
||||
IsDraft: true,
|
||||
},
|
||||
OrderID: ord.UUID,
|
||||
Status: enum.PENDING,
|
||||
SubOrders: peerOrders,
|
||||
}
|
||||
|
||||
// 1. Calcul des totaux (indépendant du mode de paiement)
|
||||
if _, err := bill.SumUpBill(request); err != nil {
|
||||
return bill, err
|
||||
}
|
||||
|
||||
// 2. Création des subscriptions pour les lignes récurrentes
|
||||
subIDs, err := createRecurringSubscriptions(bill, request)
|
||||
if err != nil {
|
||||
return bill, err
|
||||
}
|
||||
|
||||
// 3. Liaison des subscription IDs à l'order pour traçabilité
|
||||
if len(subIDs) > 0 {
|
||||
ord.SubscriptionIDs = append(ord.SubscriptionIDs, subIDs...)
|
||||
}
|
||||
|
||||
// 4. Persistance du brouillon de facture (les UnitPriceHT et SubscriptionID sont déjà set)
|
||||
stored, _, err := NewAccessor(request).StoreOne(bill)
|
||||
if err != nil {
|
||||
return bill, err
|
||||
}
|
||||
return stored.(*Bill), nil
|
||||
}
|
||||
|
||||
// createRecurringSubscriptions crée les Subscription pour chaque groupe
|
||||
// (peer × BillingStrategy) dont la stratégie est récurrente.
|
||||
// Modifie les PeerItemOrder en place (SubscriptionID).
|
||||
// Retourne les IDs de subscriptions créées.
|
||||
func createRecurringSubscriptions(b *Bill, request *tools.APIRequest) ([]string, error) {
|
||||
subIDs := []string{}
|
||||
|
||||
for peerID, po := range b.SubOrders {
|
||||
// Groupe les items récurrents par BillingStrategy
|
||||
byStrategy := map[pricing.BillingStrategy][]*PeerItemOrder{}
|
||||
for _, item := range po.Items {
|
||||
if item.BillingStrategy == pricing.BILL_ONCE {
|
||||
continue
|
||||
}
|
||||
byStrategy[item.BillingStrategy] = append(byStrategy[item.BillingStrategy], item)
|
||||
}
|
||||
|
||||
for strategy, items := range byStrategy {
|
||||
subItems := make([]*subscription.SubscriptionItem, 0, len(items))
|
||||
totalAmount := 0.0
|
||||
for _, item := range items {
|
||||
subItems = append(subItems, &subscription.SubscriptionItem{
|
||||
ResourceType: item.ResourceType,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPriceHT,
|
||||
})
|
||||
totalAmount += item.UnitPriceHT * float64(item.Quantity)
|
||||
}
|
||||
|
||||
var sub *subscription.Subscription
|
||||
switch strategy {
|
||||
case pricing.BILL_PER_YEAR:
|
||||
sub = subscription.NewYearlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
case pricing.BILL_PER_WEEK:
|
||||
sub = subscription.NewWeeklySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
default: // BILL_PER_MONTH et tout autre cas récurrent
|
||||
sub = subscription.NewMonthlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
}
|
||||
sub.IsDraft = true
|
||||
|
||||
res, _, err := subscription.NewAccessor(request).StoreOne(sub)
|
||||
if err != nil {
|
||||
return subIDs, err
|
||||
}
|
||||
storedSub := res.(*subscription.Subscription)
|
||||
subIDs = append(subIDs, storedSub.GetID())
|
||||
|
||||
// Lie le SubscriptionID à chaque ligne concernée
|
||||
for _, item := range items {
|
||||
item.SubscriptionID = storedSub.GetID()
|
||||
}
|
||||
}
|
||||
}
|
||||
return subIDs, nil
|
||||
}
|
||||
|
||||
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (r *Bill) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
}
|
||||
|
||||
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
if !r.IsDraft && r.Status != set.(*Bill).Status {
|
||||
return true, &Bill{Status: set.(*Bill).Status}
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Bill) CanDelete() bool {
|
||||
return r.IsDraft
|
||||
}
|
||||
|
||||
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
|
||||
for _, b := range d.SubOrders {
|
||||
err := b.SumUpBill(request)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
d.Total += b.Total
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PeerOrder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type PeerOrder struct {
|
||||
Error string `json:"error,omitempty" bson:"error,omitempty"`
|
||||
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
|
||||
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
|
||||
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
|
||||
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
|
||||
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
|
||||
}
|
||||
|
||||
// IsFullySettled retourne vrai si toutes les lignes de ce peer sont réglées.
|
||||
func (po *PeerOrder) IsFullySettled() bool {
|
||||
for _, item := range po.Items {
|
||||
if !item.Settled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
|
||||
switch dt {
|
||||
case tools.PROCESSING_RESOURCE:
|
||||
return &resources.PricedProcessingResource{}
|
||||
case tools.STORAGE_RESOURCE:
|
||||
return &resources.PricedStorageResource{}
|
||||
case tools.DATA_RESOURCE:
|
||||
return &resources.PricedDataResource{}
|
||||
case tools.COMPUTE_RESOURCE:
|
||||
return &resources.PricedComputeResource{}
|
||||
case tools.WORKFLOW_RESOURCE:
|
||||
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
|
||||
d.Status = enum.PENDING
|
||||
go func() {
|
||||
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER — UPDATE STATUS ON RETURN
|
||||
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
|
||||
if d.Status == enum.PAID {
|
||||
now := time.Now().UTC()
|
||||
for _, b := range d.Items {
|
||||
priced := PricedByType(b.ResourceType)
|
||||
bb, _ := json.Marshal(b.Item)
|
||||
json.Unmarshal(bb, priced)
|
||||
if !priced.IsPurchasable() {
|
||||
continue
|
||||
}
|
||||
accessor := purchase_resource.NewAccessor(request)
|
||||
accessor.StoreOne(&purchase_resource.PurchaseResource{
|
||||
ResourceID: priced.GetID(),
|
||||
ResourceType: priced.GetType(),
|
||||
EndDate: priced.GetLocationEnd(),
|
||||
})
|
||||
// Marque la ligne comme réglée
|
||||
b.Settled = true
|
||||
b.SettledAt = &now
|
||||
}
|
||||
}
|
||||
if d.Status != enum.PENDING {
|
||||
response <- d
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
|
||||
for _, b := range d.Items {
|
||||
tot, err := b.GetPriceHT(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Total += tot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PeerItemOrder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PeerItemOrder est une ligne de facture pour un peer donné.
|
||||
type PeerItemOrder struct {
|
||||
// ItemID identifie de manière unique cette ligne pour le suivi de règlement.
|
||||
ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"`
|
||||
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
|
||||
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
|
||||
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
|
||||
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
|
||||
|
||||
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
|
||||
|
||||
// UnitPriceHT est le prix unitaire HT calculé par SumUpBill/GetPriceHT.
|
||||
// Utilisé pour la création des subscriptions sans recalcul.
|
||||
UnitPriceHT float64 `json:"unit_price_ht,omitempty" bson:"unit_price_ht,omitempty"`
|
||||
|
||||
// SubscriptionID référence la Subscription créée pour les lignes récurrentes.
|
||||
// Vide pour les lignes BILL_ONCE.
|
||||
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
|
||||
|
||||
// Settled indique si cette ligne a été réglée (paiement confirmé).
|
||||
Settled bool `json:"settled" bson:"settled"`
|
||||
SettledAt *time.Time `json:"settled_at,omitempty" bson:"settled_at,omitempty"`
|
||||
}
|
||||
|
||||
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
|
||||
if d.Purchase == nil {
|
||||
return 0, nil
|
||||
}
|
||||
priced := PricedByType(d.ResourceType)
|
||||
b, _ := json.Marshal(d.Item)
|
||||
err := json.Unmarshal(b, priced)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
accessor := purchase_resource.NewAccessor(request)
|
||||
search, code, _ := accessor.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
|
||||
},
|
||||
}, "", d.Purchase.IsDraft, 0, 10000)
|
||||
if code == 200 && len(search) > 0 {
|
||||
for _, s := range search {
|
||||
if s.(*purchase_resource.PurchaseResource).EndDate == nil ||
|
||||
time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
unitPrice, err := priced.GetPriceHT()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.UnitPriceHT = unitPrice // cache pour createRecurringSubscriptions
|
||||
return unitPrice * float64(d.Quantity), nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package bill
|
||||
package billing
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
@@ -7,14 +7,13 @@ import (
|
||||
)
|
||||
|
||||
type billMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Bill] // AbstractAccessor contains the basic fields of an accessor (model, caller)
|
||||
utils.AbstractAccessor[*Bill]
|
||||
}
|
||||
|
||||
// New creates a new instance of the billMongoAccessor
|
||||
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
|
||||
return &billMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Bill]{
|
||||
Logger: logs.CreateLogger(tools.BILL.String()), // Create a logger with the data type
|
||||
Logger: logs.CreateLogger(tools.BILL.String()),
|
||||
Request: request,
|
||||
Type: tools.BILL,
|
||||
New: func() *Bill { return &Bill{} },
|
||||
@@ -0,0 +1,146 @@
|
||||
package discount
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type DiscountType int
|
||||
|
||||
const (
|
||||
PERCENTAGE DiscountType = iota // réduction en pourcentage
|
||||
FIXED_AMOUNT // réduction montant fixe
|
||||
)
|
||||
|
||||
func (d DiscountType) String() string {
|
||||
return [...]string{"percentage", "fixed_amount"}[d]
|
||||
}
|
||||
|
||||
func DiscountTypeList() []DiscountType {
|
||||
return []DiscountType{PERCENTAGE, FIXED_AMOUNT}
|
||||
}
|
||||
|
||||
type DiscountScope int
|
||||
|
||||
const (
|
||||
SCOPE_ALL DiscountScope = iota // applicable à tout
|
||||
SCOPE_RESOURCE_TYPE // applicable à un type de ressource
|
||||
SCOPE_RESOURCE // applicable à une ressource spécifique
|
||||
SCOPE_SUBSCRIPTION // applicable aux souscriptions
|
||||
)
|
||||
|
||||
func (d DiscountScope) String() string {
|
||||
return [...]string{"all", "resource_type", "resource", "subscription"}[d]
|
||||
}
|
||||
|
||||
// Discount représente une réduction applicable sur les ressources ou abonnements.
|
||||
type Discount struct {
|
||||
utils.AbstractObject
|
||||
Code string `json:"code,omitempty" bson:"code,omitempty"`
|
||||
DiscountType DiscountType `json:"discount_type" bson:"discount_type"`
|
||||
Scope DiscountScope `json:"scope" bson:"scope"`
|
||||
Value float64 `json:"value" bson:"value"` // pourcentage (0-100) ou montant fixe
|
||||
Currency string `json:"currency,omitempty" bson:"currency,omitempty"` // pour FIXED_AMOUNT
|
||||
ResourceTypes []tools.DataType `json:"resource_types,omitempty" bson:"resource_types,omitempty"` // si SCOPE_RESOURCE_TYPE
|
||||
ResourceIDs []string `json:"resource_ids,omitempty" bson:"resource_ids,omitempty"` // si SCOPE_RESOURCE
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
|
||||
MaxUsage int `json:"max_usage,omitempty" bson:"max_usage,omitempty"` // 0 = illimité
|
||||
CurrentUsage int `json:"current_usage" bson:"current_usage"`
|
||||
MinAmount float64 `json:"min_amount,omitempty" bson:"min_amount,omitempty"` // montant minimum du bill pour appliquer
|
||||
Active bool `json:"active" bson:"active" default:"true"`
|
||||
}
|
||||
|
||||
// IsValid vérifie si la réduction est applicable au moment présent.
|
||||
func (d *Discount) IsValid(billAmount float64) bool {
|
||||
now := time.Now().UTC()
|
||||
if !d.Active {
|
||||
return false
|
||||
}
|
||||
if d.MaxUsage > 0 && d.CurrentUsage >= d.MaxUsage {
|
||||
return false
|
||||
}
|
||||
if d.ValidFrom != nil && now.Before(*d.ValidFrom) {
|
||||
return false
|
||||
}
|
||||
if d.ValidUntil != nil && now.After(*d.ValidUntil) {
|
||||
return false
|
||||
}
|
||||
if d.MinAmount > 0 && billAmount < d.MinAmount {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Apply applique la réduction sur un prix HT et retourne le prix réduit.
|
||||
func (d *Discount) Apply(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT - (priceHT * d.Value / 100)
|
||||
case FIXED_AMOUNT:
|
||||
result := priceHT - d.Value
|
||||
if result < 0 {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
return priceHT
|
||||
}
|
||||
|
||||
// DiscountAmount retourne le montant de la réduction sans l'appliquer.
|
||||
func (d *Discount) DiscountAmount(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT * d.Value / 100
|
||||
case FIXED_AMOUNT:
|
||||
if d.Value > priceHT {
|
||||
return priceHT
|
||||
}
|
||||
return d.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AppliesToResource vérifie si cette réduction s'applique à une ressource donnée.
|
||||
func (d *Discount) AppliesToResource(resourceID string, resourceType tools.DataType) bool {
|
||||
switch d.Scope {
|
||||
case SCOPE_ALL:
|
||||
return true
|
||||
case SCOPE_RESOURCE:
|
||||
for _, id := range d.ResourceIDs {
|
||||
if id == resourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case SCOPE_RESOURCE_TYPE:
|
||||
for _, t := range d.ResourceTypes {
|
||||
if t == resourceType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IncrementUsage incrémente le compteur d'utilisation.
|
||||
func (d *Discount) IncrementUsage() {
|
||||
d.CurrentUsage++
|
||||
}
|
||||
|
||||
func (d *Discount) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (d *Discount) StoreDraftDefault() {
|
||||
d.IsDraft = true
|
||||
}
|
||||
|
||||
func (d *Discount) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
return d.IsDraft, set
|
||||
}
|
||||
|
||||
func (d *Discount) CanDelete() bool {
|
||||
return d.IsDraft || d.CurrentUsage == 0
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package discount
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type discountMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Discount]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *discountMongoAccessor {
|
||||
return &discountMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Discount]{
|
||||
Logger: logs.CreateLogger(tools.DISCOUNT.String()),
|
||||
Request: request,
|
||||
Type: tools.DISCOUNT,
|
||||
New: func() *Discount { return &Discount{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type PaymentStatus int
|
||||
|
||||
const (
|
||||
PAYMENT_PENDING PaymentStatus = iota
|
||||
PAYMENT_PROCESSING // en cours de traitement blockchain/réseau
|
||||
PAYMENT_COMPLETED // confirmé
|
||||
PAYMENT_FAILED // échoué
|
||||
PAYMENT_CANCELLED // annulé avant exécution
|
||||
PAYMENT_REFUNDED // remboursé
|
||||
)
|
||||
|
||||
func (s PaymentStatus) String() string {
|
||||
return [...]string{"pending", "processing", "completed", "failed", "cancelled", "refunded"}[s]
|
||||
}
|
||||
|
||||
func PaymentStatusList() []PaymentStatus {
|
||||
return []PaymentStatus{PAYMENT_PENDING, PAYMENT_PROCESSING, PAYMENT_COMPLETED, PAYMENT_FAILED, PAYMENT_CANCELLED, PAYMENT_REFUNDED}
|
||||
}
|
||||
|
||||
type PaymentMethod int
|
||||
|
||||
const (
|
||||
METHOD_BLOCKCHAIN PaymentMethod = iota
|
||||
METHOD_CREDIT_CARD
|
||||
METHOD_BANK_TRANSFER
|
||||
METHOD_CRYPTO
|
||||
METHOD_INTERNAL_CREDIT // crédit interne à la plateforme
|
||||
)
|
||||
|
||||
func (m PaymentMethod) String() string {
|
||||
return [...]string{"blockchain", "credit_card", "bank_transfer", "crypto", "internal_credit"}[m]
|
||||
}
|
||||
|
||||
func PaymentMethodList() []PaymentMethod {
|
||||
return []PaymentMethod{METHOD_BLOCKCHAIN, METHOD_CREDIT_CARD, METHOD_BANK_TRANSFER, METHOD_CRYPTO, METHOD_INTERNAL_CREDIT}
|
||||
}
|
||||
|
||||
// Payment représente une transaction de paiement — instantanée, mensuelle ou annuelle.
|
||||
type Payment struct {
|
||||
utils.AbstractObject
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
|
||||
PayerPeerID string `json:"payer_peer_id,omitempty" bson:"payer_peer_id,omitempty"`
|
||||
RecipientPeerID string `json:"recipient_peer_id,omitempty" bson:"recipient_peer_id,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Status PaymentStatus `json:"status" bson:"status"`
|
||||
Method PaymentMethod `json:"method" bson:"method"`
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"` // PAY_ONCE, PAY_EVERY_MONTH, PAY_EVERY_YEAR
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
WalletFrom string `json:"wallet_from,omitempty" bson:"wallet_from,omitempty"`
|
||||
WalletTo string `json:"wallet_to,omitempty" bson:"wallet_to,omitempty"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty" bson:"scheduled_at,omitempty"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty" bson:"failure_reason,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" bson:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// NewInstantPayment crée un paiement immédiat (PAY_ONCE).
|
||||
func NewInstantPayment(billID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod) *Payment {
|
||||
return &Payment{
|
||||
BillID: billID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: pricing.PAY_ONCE,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScheduledPayment crée un paiement programmé (mensuel ou annuel).
|
||||
func NewScheduledPayment(subscriptionID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod, paymentType pricing.PaymentType, scheduledAt time.Time) *Payment {
|
||||
return &Payment{
|
||||
SubscriptionID: subscriptionID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: paymentType,
|
||||
ScheduledAt: &scheduledAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Complete marque le paiement comme confirmé.
|
||||
func (p *Payment) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_COMPLETED
|
||||
p.TransactionID = transactionID
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Fail marque le paiement comme échoué.
|
||||
func (p *Payment) Fail(reason string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_FAILED
|
||||
p.FailureReason = reason
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule le paiement s'il est encore en attente.
|
||||
func (p *Payment) Cancel() bool {
|
||||
if p.Status != PAYMENT_PENDING {
|
||||
return false
|
||||
}
|
||||
p.Status = PAYMENT_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRefundable indique si le paiement peut faire l'objet d'un remboursement.
|
||||
func (p *Payment) IsRefundable() bool {
|
||||
return p.Status == PAYMENT_COMPLETED
|
||||
}
|
||||
|
||||
func (p *Payment) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (p *Payment) StoreDraftDefault() {
|
||||
p.IsDraft = true
|
||||
}
|
||||
|
||||
func (p *Payment) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Payment)
|
||||
if !p.IsDraft && p.Status != incoming.Status {
|
||||
return true, &Payment{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
FailureReason: incoming.FailureReason,
|
||||
}
|
||||
}
|
||||
return p.IsDraft, set
|
||||
}
|
||||
|
||||
func (p *Payment) CanDelete() bool {
|
||||
return p.IsDraft || p.Status == PAYMENT_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type paymentMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Payment]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *paymentMongoAccessor {
|
||||
return &paymentMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Payment]{
|
||||
Logger: logs.CreateLogger(tools.PAYMENT.String()),
|
||||
Request: request,
|
||||
Type: tools.PAYMENT,
|
||||
New: func() *Payment { return &Payment{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
)
|
||||
|
||||
type ScheduleStatus int
|
||||
|
||||
const (
|
||||
SCHEDULE_ACTIVE ScheduleStatus = iota
|
||||
SCHEDULE_PAUSED // mis en pause manuellement
|
||||
SCHEDULE_CANCELLED // résilié
|
||||
SCHEDULE_COMPLETED // terminé normalement (abonnement expiré)
|
||||
SCHEDULE_FAILED // trop d'échecs consécutifs
|
||||
)
|
||||
|
||||
func (s ScheduleStatus) String() string {
|
||||
return [...]string{"active", "paused", "cancelled", "completed", "failed"}[s]
|
||||
}
|
||||
|
||||
// PaymentSchedule pilote la récurrence des paiements d'un abonnement.
|
||||
type PaymentSchedule struct {
|
||||
SubscriptionID string `json:"subscription_id" bson:"subscription_id"`
|
||||
Frequency pricing.PaymentType `json:"frequency" bson:"frequency"` // PAY_EVERY_WEEK / PAY_EVERY_MONTH / PAY_EVERY_YEAR
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency"`
|
||||
Status ScheduleStatus `json:"status" bson:"status"`
|
||||
NextPaymentDate time.Time `json:"next_payment_date" bson:"next_payment_date"`
|
||||
LastExecutedAt *time.Time `json:"last_executed_at,omitempty" bson:"last_executed_at,omitempty"`
|
||||
FailureCount int `json:"failure_count" bson:"failure_count"`
|
||||
MaxRetries int `json:"max_retries" bson:"max_retries" default:"3"`
|
||||
}
|
||||
|
||||
// nextDate calcule la prochaine date selon la fréquence.
|
||||
func (ps *PaymentSchedule) nextDate() time.Time {
|
||||
switch ps.Frequency {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
return ps.NextPaymentDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
return ps.NextPaymentDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
return ps.NextPaymentDate.AddDate(1, 0, 0)
|
||||
}
|
||||
return ps.NextPaymentDate
|
||||
}
|
||||
|
||||
// Advance enregistre l'exécution réussie et avance à la prochaine échéance.
|
||||
func (ps *PaymentSchedule) Advance() {
|
||||
now := time.Now().UTC()
|
||||
ps.LastExecutedAt = &now
|
||||
ps.FailureCount = 0
|
||||
ps.NextPaymentDate = ps.nextDate()
|
||||
}
|
||||
|
||||
// RecordFailure incrémente le compteur d'échecs et désactive après MaxRetries.
|
||||
func (ps *PaymentSchedule) RecordFailure() {
|
||||
ps.FailureCount++
|
||||
if ps.MaxRetries > 0 && ps.FailureCount >= ps.MaxRetries {
|
||||
ps.Status = SCHEDULE_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
// IsDue retourne vrai si le paiement est dû maintenant.
|
||||
func (ps *PaymentSchedule) IsDue() bool {
|
||||
return ps.Status == SCHEDULE_ACTIVE && !time.Now().UTC().Before(ps.NextPaymentDate)
|
||||
}
|
||||
|
||||
// Pause suspend temporairement le calendrier.
|
||||
func (ps *PaymentSchedule) Pause() {
|
||||
if ps.Status == SCHEDULE_ACTIVE {
|
||||
ps.Status = SCHEDULE_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un calendrier mis en pause.
|
||||
func (ps *PaymentSchedule) Resume() {
|
||||
if ps.Status == SCHEDULE_PAUSED {
|
||||
ps.Status = SCHEDULE_ACTIVE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type RefundStatus int
|
||||
|
||||
const (
|
||||
REFUND_PENDING RefundStatus = iota
|
||||
REFUND_APPROVED // approuvé, en attente de traitement
|
||||
REFUND_REJECTED // rejeté
|
||||
REFUND_PROCESSING // en cours de virement/blockchain
|
||||
REFUND_COMPLETED // remboursé
|
||||
REFUND_CANCELLED // annulé avant approbation
|
||||
)
|
||||
|
||||
func (s RefundStatus) String() string {
|
||||
return [...]string{"pending", "approved", "rejected", "processing", "completed", "cancelled"}[s]
|
||||
}
|
||||
|
||||
func RefundStatusList() []RefundStatus {
|
||||
return []RefundStatus{REFUND_PENDING, REFUND_APPROVED, REFUND_REJECTED, REFUND_PROCESSING, REFUND_COMPLETED, REFUND_CANCELLED}
|
||||
}
|
||||
|
||||
// Refund représente une demande de remboursement sur un paiement validé.
|
||||
type Refund struct {
|
||||
utils.AbstractObject
|
||||
PaymentID string `json:"payment_id" bson:"payment_id" validate:"required"`
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
RefundType pricing.RefundType `json:"refund_type" bson:"refund_type"`
|
||||
Amount float64 `json:"amount" bson:"amount" validate:"required"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Reason string `json:"reason,omitempty" bson:"reason,omitempty"`
|
||||
Status RefundStatus `json:"status" bson:"status"`
|
||||
RequestedAt time.Time `json:"requested_at" bson:"requested_at"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
ProcessedByID string `json:"processed_by_id,omitempty" bson:"processed_by_id,omitempty"`
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
Notes string `json:"notes,omitempty" bson:"notes,omitempty"`
|
||||
// ratio appliqué sur le montant original (0-100). 0 = non renseigné (remboursement total).
|
||||
RefundRatio float64 `json:"refund_ratio,omitempty" bson:"refund_ratio,omitempty"`
|
||||
}
|
||||
|
||||
// NewRefund crée une demande de remboursement total.
|
||||
func NewRefund(paymentID, billID string, amount float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
return &Refund{
|
||||
PaymentID: paymentID,
|
||||
BillID: billID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
RefundType: refundType,
|
||||
Reason: reason,
|
||||
Status: REFUND_PENDING,
|
||||
RequestedAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewPartialRefund crée une demande de remboursement partiel selon un ratio pourcentage.
|
||||
func NewPartialRefund(paymentID, billID string, originalAmount, ratioPercent float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
amount := originalAmount * ratioPercent / 100
|
||||
r := NewRefund(paymentID, billID, amount, currency, refundType, reason)
|
||||
r.RefundRatio = ratioPercent
|
||||
return r
|
||||
}
|
||||
|
||||
// Approve approuve la demande de remboursement.
|
||||
func (r *Refund) Approve(processedByID string) {
|
||||
r.Status = REFUND_APPROVED
|
||||
r.ProcessedByID = processedByID
|
||||
}
|
||||
|
||||
// Reject rejette la demande de remboursement.
|
||||
func (r *Refund) Reject(processedByID, notes string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_REJECTED
|
||||
r.ProcessedByID = processedByID
|
||||
r.ProcessedAt = &now
|
||||
r.Notes = notes
|
||||
}
|
||||
|
||||
// Process passe le remboursement en cours de traitement.
|
||||
func (r *Refund) Process() bool {
|
||||
if r.Status != REFUND_APPROVED {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_PROCESSING
|
||||
return true
|
||||
}
|
||||
|
||||
// Complete finalise le remboursement avec l'identifiant de transaction.
|
||||
func (r *Refund) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_COMPLETED
|
||||
r.TransactionID = transactionID
|
||||
r.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule la demande si elle est encore en attente.
|
||||
func (r *Refund) Cancel() bool {
|
||||
if r.Status != REFUND_PENDING {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Refund) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (r *Refund) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
}
|
||||
|
||||
func (r *Refund) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Refund)
|
||||
if !r.IsDraft && r.Status != incoming.Status {
|
||||
return true, &Refund{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
Notes: incoming.Notes,
|
||||
ProcessedByID: incoming.ProcessedByID,
|
||||
}
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Refund) CanDelete() bool {
|
||||
return r.IsDraft || r.Status == REFUND_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type refundMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Refund]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *refundMongoAccessor {
|
||||
return &refundMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Refund]{
|
||||
Logger: logs.CreateLogger(tools.REFUND.String()),
|
||||
Request: request,
|
||||
Type: tools.REFUND,
|
||||
New: func() *Refund { return &Refund{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type SubscriptionStatus int
|
||||
|
||||
const (
|
||||
SUBSCRIPTION_PENDING SubscriptionStatus = iota // en attente de premier paiement
|
||||
SUBSCRIPTION_TRIAL // période d'essai
|
||||
SUBSCRIPTION_ACTIVE // actif
|
||||
SUBSCRIPTION_PAUSED // suspendu temporairement
|
||||
SUBSCRIPTION_CANCELLED // résilié par l'utilisateur
|
||||
SUBSCRIPTION_EXPIRED // date de fin dépassée
|
||||
)
|
||||
|
||||
func (s SubscriptionStatus) String() string {
|
||||
return [...]string{"pending", "trial", "active", "paused", "cancelled", "expired"}[s]
|
||||
}
|
||||
|
||||
func SubscriptionStatusList() []SubscriptionStatus {
|
||||
return []SubscriptionStatus{SUBSCRIPTION_PENDING, SUBSCRIPTION_TRIAL, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_EXPIRED}
|
||||
}
|
||||
|
||||
// SubscriptionItem représente un élément d'un abonnement (ressource louée).
|
||||
type SubscriptionItem struct {
|
||||
ResourceID string `json:"resource_id" bson:"resource_id"`
|
||||
ResourceType tools.DataType `json:"resource_type" bson:"resource_type"`
|
||||
Quantity int `json:"quantity" bson:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
|
||||
}
|
||||
|
||||
// Subscription représente un abonnement mensuel ou annuel à des ressources.
|
||||
type Subscription struct {
|
||||
utils.AbstractObject
|
||||
SubscriberPeerID string `json:"subscriber_peer_id" bson:"subscriber_peer_id" validate:"required"`
|
||||
ProviderPeerID string `json:"provider_peer_id,omitempty" bson:"provider_peer_id,omitempty"`
|
||||
Status SubscriptionStatus `json:"status" bson:"status"`
|
||||
PlanType pricing.PaymentType `json:"plan_type" bson:"plan_type"` // PAY_EVERY_MONTH ou PAY_EVERY_YEAR
|
||||
Items []*SubscriptionItem `json:"items,omitempty" bson:"items,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
StartDate time.Time `json:"start_date" bson:"start_date"`
|
||||
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
|
||||
NextBillingDate time.Time `json:"next_billing_date" bson:"next_billing_date"`
|
||||
AutoRenew bool `json:"auto_renew" bson:"auto_renew" default:"true"`
|
||||
TrialEndDate *time.Time `json:"trial_end_date,omitempty" bson:"trial_end_date,omitempty"`
|
||||
DiscountIDs []string `json:"discount_ids,omitempty" bson:"discount_ids,omitempty"`
|
||||
CancelledAt *time.Time `json:"cancelled_at,omitempty" bson:"cancelled_at,omitempty"`
|
||||
CancelReason string `json:"cancel_reason,omitempty" bson:"cancel_reason,omitempty"`
|
||||
}
|
||||
|
||||
// newSubscription est le constructeur interne commun.
|
||||
func newSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, plan pricing.PaymentType, nextBilling time.Time) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return &Subscription{
|
||||
SubscriberPeerID: subscriberPeerID,
|
||||
ProviderPeerID: providerPeerID,
|
||||
Status: SUBSCRIPTION_PENDING,
|
||||
PlanType: plan,
|
||||
Items: items,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
StartDate: now,
|
||||
NextBillingDate: nextBilling,
|
||||
AutoRenew: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWeeklySubscription crée un abonnement hebdomadaire.
|
||||
func NewWeeklySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_WEEK, now.AddDate(0, 0, 7))
|
||||
}
|
||||
|
||||
// NewMonthlySubscription crée un abonnement mensuel.
|
||||
func NewMonthlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_MONTH, now.AddDate(0, 1, 0))
|
||||
}
|
||||
|
||||
// NewYearlySubscription crée un abonnement annuel.
|
||||
func NewYearlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_YEAR, now.AddDate(1, 0, 0))
|
||||
}
|
||||
|
||||
// NewTrialSubscription crée un abonnement mensuel avec période d'essai.
|
||||
func NewTrialSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, trialDays int) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
trialEnd := now.AddDate(0, 0, trialDays)
|
||||
s := NewMonthlySubscription(subscriberPeerID, providerPeerID, items, amount, currency)
|
||||
s.Status = SUBSCRIPTION_TRIAL
|
||||
s.TrialEndDate = &trialEnd
|
||||
s.NextBillingDate = trialEnd
|
||||
return s
|
||||
}
|
||||
|
||||
// Activate passe l'abonnement au statut actif (après premier paiement).
|
||||
func (s *Subscription) Activate() {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
|
||||
// Pause suspend l'abonnement.
|
||||
func (s *Subscription) Pause() {
|
||||
if s.Status == SUBSCRIPTION_ACTIVE {
|
||||
s.Status = SUBSCRIPTION_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un abonnement suspendu.
|
||||
func (s *Subscription) Resume() {
|
||||
if s.Status == SUBSCRIPTION_PAUSED {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel résilie l'abonnement.
|
||||
func (s *Subscription) Cancel(reason string) {
|
||||
now := time.Now().UTC()
|
||||
s.Status = SUBSCRIPTION_CANCELLED
|
||||
s.CancelledAt = &now
|
||||
s.CancelReason = reason
|
||||
s.AutoRenew = false
|
||||
}
|
||||
|
||||
// Renew avance la prochaine date de facturation d'une période.
|
||||
func (s *Subscription) Renew() {
|
||||
switch s.PlanType {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsExpired vérifie si l'abonnement a dépassé sa date de fin.
|
||||
func (s *Subscription) IsExpired() bool {
|
||||
if s.EndDate == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().UTC().After(*s.EndDate)
|
||||
}
|
||||
|
||||
// IsBillingDue vérifie si la prochaine échéance est atteinte.
|
||||
func (s *Subscription) IsBillingDue() bool {
|
||||
return s.Status == SUBSCRIPTION_ACTIVE && !time.Now().UTC().Before(s.NextBillingDate)
|
||||
}
|
||||
|
||||
// IsInTrial vérifie si l'abonnement est en période d'essai.
|
||||
func (s *Subscription) IsInTrial() bool {
|
||||
return s.Status == SUBSCRIPTION_TRIAL && s.TrialEndDate != nil && time.Now().UTC().Before(*s.TrialEndDate)
|
||||
}
|
||||
|
||||
// ComputeAmount recalcule le montant total depuis les items.
|
||||
func (s *Subscription) ComputeAmount() float64 {
|
||||
total := 0.0
|
||||
for _, item := range s.Items {
|
||||
total += item.UnitPrice * float64(item.Quantity)
|
||||
}
|
||||
s.Amount = total
|
||||
return total
|
||||
}
|
||||
|
||||
func (s *Subscription) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (s *Subscription) StoreDraftDefault() {
|
||||
s.IsDraft = true
|
||||
}
|
||||
|
||||
func (s *Subscription) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Subscription)
|
||||
if !s.IsDraft && s.Status != incoming.Status {
|
||||
return true, &Subscription{
|
||||
Status: incoming.Status,
|
||||
AutoRenew: incoming.AutoRenew,
|
||||
CancelReason: incoming.CancelReason,
|
||||
}
|
||||
}
|
||||
return s.IsDraft, set
|
||||
}
|
||||
|
||||
func (s *Subscription) CanDelete() bool {
|
||||
return s.IsDraft || s.Status == SUBSCRIPTION_CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type subscriptionMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Subscription]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *subscriptionMongoAccessor {
|
||||
return &subscriptionMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Subscription]{
|
||||
Logger: logs.CreateLogger(tools.SUBSCRIPTION.String()),
|
||||
Request: request,
|
||||
Type: tools.SUBSCRIPTION,
|
||||
New: func() *Subscription { return &Subscription{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package bill_test
|
||||
package billing_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/bill"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
@@ -12,71 +12,67 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ---- Bill model ----
|
||||
|
||||
func TestBill_StoreDraftDefault(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.StoreDraftDefault()
|
||||
assert.True(t, b.IsDraft)
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_Draft(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = true
|
||||
assert.True(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_NonDraft(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = false
|
||||
assert.False(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &bill.Bill{Status: enum.PAID}
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, returned := b.CanUpdate(set)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, enum.PAID, returned.(*bill.Bill).Status)
|
||||
assert.Equal(t, enum.PAID, returned.(*billing.Bill).Status)
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &bill.Bill{Status: enum.PENDING}
|
||||
set := &billing.Bill{Status: enum.PENDING}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_Draft(t *testing.T) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = true
|
||||
set := &bill.Bill{Status: enum.PAID}
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(&tools.APIRequest{})
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor_NilRequest(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(nil)
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
// ---- GenerateBill ----
|
||||
|
||||
func TestGenerateBill_Basic(t *testing.T) {
|
||||
o := &order.Order{
|
||||
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
|
||||
}
|
||||
req := &tools.APIRequest{PeerID: "peer-abc"}
|
||||
b, err := bill.GenerateBill(o, req)
|
||||
b, err := billing.GenerateBill(o, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, b)
|
||||
assert.Equal(t, "order-uuid-1", b.OrderID)
|
||||
@@ -85,10 +81,8 @@ func TestGenerateBill_Basic(t *testing.T) {
|
||||
assert.Contains(t, b.Name, "peer-abc")
|
||||
}
|
||||
|
||||
// ---- SumUpBill ----
|
||||
|
||||
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
|
||||
b := &bill.Bill{Total: 0}
|
||||
b := &billing.Bill{Total: 0}
|
||||
result, err := b.SumUpBill(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0.0, result.Total)
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
|
||||
/*
|
||||
* Booking is a struct that represents a booking
|
||||
*/
|
||||
@@ -48,6 +50,16 @@ type Booking struct {
|
||||
// OriginRef carries the registry reference of a peerless resource
|
||||
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
|
||||
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"`
|
||||
|
||||
// BillingStrategy est la fréquence de facturation appliquée à ce booking
|
||||
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
|
||||
// Transmis depuis WorkflowExecution.SelectedBillingStrategy lors du Book().
|
||||
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
|
||||
|
||||
// PaymentType est le mode de paiement choisi pour cette ressource spécifique
|
||||
// (PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR).
|
||||
// Résolu depuis WorkflowExecution.SelectedPaymentMode[itemID] lors du Book().
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
|
||||
}
|
||||
|
||||
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
|
||||
|
||||
+8
-18
@@ -1,23 +1,13 @@
|
||||
package booking
|
||||
|
||||
type BookingMode int
|
||||
import "cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
|
||||
// BookingMode is kept here as an alias for backward compatibility.
|
||||
// The canonical definition lives in models/common/enum.
|
||||
type BookingMode = enum.BookingMode
|
||||
|
||||
const (
|
||||
PLANNED BookingMode = iota // predictible
|
||||
PREEMPTED // can be both predictible or unpredictible, first one asking for a quick exec, second on event, but we pay to preempt in any case.
|
||||
WHEN_POSSIBLE // unpredictable, two mode of payment can be available on that case: fixed, or per USE
|
||||
PLANNED = enum.PLANNED
|
||||
PREEMPTED = enum.PREEMPTED
|
||||
WHEN_POSSIBLE = enum.WHEN_POSSIBLE
|
||||
)
|
||||
|
||||
/*
|
||||
Ok make a point there:
|
||||
There is 3 notions about booking & payment :
|
||||
Booking mode : WHEN is executed
|
||||
Buying mode : Duration of payment
|
||||
Pricing Mode : How Many time we pay
|
||||
|
||||
|
||||
We can simplify Buying Mode and Pricing Mode, some Buying Mode implied limited pricing mode
|
||||
Such as Rules. Just like PERMANENT BUYING can be paid only once.
|
||||
|
||||
Booking Mode on WHEN POSSIBLE make an exception, because we can't know when executed.
|
||||
*/
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
|
||||
// InstanceCapacity holds the maximum available resources of a single resource instance.
|
||||
type InstanceCapacity struct {
|
||||
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
|
||||
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
|
||||
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
|
||||
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
|
||||
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
|
||||
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
|
||||
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
|
||||
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
|
||||
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ type PlannerITF interface {
|
||||
// any availability check against a blocked resource returns false immediately.
|
||||
type Planner struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
|
||||
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
|
||||
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
|
||||
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
|
||||
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package enum
|
||||
|
||||
type BookingMode int
|
||||
|
||||
const (
|
||||
PLANNED BookingMode = iota // timing prédictible
|
||||
PREEMPTED // peut être interrompu, premium payé
|
||||
WHEN_POSSIBLE // timing imprévisible
|
||||
)
|
||||
|
||||
/*
|
||||
3 notions distinctes :
|
||||
- BookingMode : QUAND est exécuté (PLANNED / PREEMPTED / WHEN_POSSIBLE)
|
||||
- BillingStrategy : fréquence de facturation (ONCE / WEEKLY / MONTHLY / YEARLY)
|
||||
- PaymentType : mode de paiement par ressource (PAY_ONCE / PAY_EVERY_MONTH / ...)
|
||||
*/
|
||||
@@ -1,5 +1,12 @@
|
||||
package models
|
||||
|
||||
type PathSource struct {
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Image is the container image TEMPO
|
||||
IsReachable bool `json:"is_reachable,omitempty" bson:"is_reachable,omitempty"`
|
||||
Args string `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
|
||||
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
|
||||
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
|
||||
|
||||
@@ -3,7 +3,7 @@ package pricing
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ type PricedItemITF interface {
|
||||
IsBooked() bool
|
||||
GetQuantity() int
|
||||
AddQuantity(amount int)
|
||||
GetBookingMode() booking.BookingMode
|
||||
GetBookingMode() enum.BookingMode
|
||||
GetCreatorID() string
|
||||
SelectPricing() PricingProfileITF
|
||||
GetLocationStart() *time.Time
|
||||
|
||||
@@ -111,13 +111,11 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
|
||||
|
||||
fromAverageDuration := after.Sub(now).Seconds()
|
||||
var tEnd time.Time
|
||||
if end == nil {
|
||||
tEnd = start.Add(5 * time.Minute)
|
||||
} else {
|
||||
fromDateDuration := float64(0)
|
||||
if end != nil {
|
||||
tEnd = *end
|
||||
fromDateDuration = tEnd.Sub(start).Seconds()
|
||||
}
|
||||
fromDateDuration := tEnd.Sub(start).Seconds()
|
||||
|
||||
if fromAverageDuration > fromDateDuration {
|
||||
return fromAverageDuration
|
||||
}
|
||||
@@ -126,6 +124,9 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
|
||||
|
||||
func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInSecond float64, start time.Time, end *time.Time) (float64, error) {
|
||||
locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end)
|
||||
if locationDurationInSecond <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
priceStr := fmt.Sprintf("%v", price)
|
||||
p, err := strconv.ParseFloat(priceStr, 64)
|
||||
if err != nil {
|
||||
@@ -176,7 +177,6 @@ func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration
|
||||
}
|
||||
|
||||
return p.Price, nil
|
||||
|
||||
case PERMANENT:
|
||||
if variations != nil {
|
||||
price := p.Price
|
||||
|
||||
+10
-2
@@ -3,7 +3,11 @@ package models
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/allowed_image"
|
||||
"cloud.o-forge.io/core/oc-lib/models/bill"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/execution_verification"
|
||||
"cloud.o-forge.io/core/oc-lib/models/live"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
@@ -47,7 +51,11 @@ var ModelsCatalog = map[string]func() utils.DBObject{
|
||||
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
|
||||
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
|
||||
tools.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} },
|
||||
tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} },
|
||||
tools.BILL.String(): func() utils.DBObject { return &billing.Bill{} },
|
||||
tools.PAYMENT.String(): func() utils.DBObject { return &payment.Payment{} },
|
||||
tools.REFUND.String(): func() utils.DBObject { return &refund.Refund{} },
|
||||
tools.DISCOUNT.String(): func() utils.DBObject { return &discount.Discount{} },
|
||||
tools.SUBSCRIPTION.String(): func() utils.DBObject { return &subscription.Subscription{} },
|
||||
tools.EXECUTION_VERIFICATION.String(): func() utils.DBObject { return &execution_verification.ExecutionVerification{} },
|
||||
tools.ALLOWED_IMAGE.String(): func() utils.DBObject { return &allowed_image.AllowedImage{} },
|
||||
}
|
||||
|
||||
@@ -22,7 +22,12 @@ type Order struct {
|
||||
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
|
||||
Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
|
||||
|
||||
// Billing groupe les bookings par fréquence de facturation, peuplé par GenerateOrder.
|
||||
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"`
|
||||
|
||||
// SubscriptionIDs liste les abonnements récurrents créés pour cet order
|
||||
// (un par peer × stratégie de facturation). Peuplé par DraftFirstBill.
|
||||
SubscriptionIDs []string `json:"subscription_ids,omitempty" bson:"subscription_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Order) StoreDraftDefault() {
|
||||
|
||||
@@ -62,7 +62,7 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
|
||||
|
||||
type DataInstance struct {
|
||||
ResourceInstance[*DataResourcePartnership]
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
|
||||
}
|
||||
|
||||
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package resources
|
||||
|
||||
// exploitation_authorization.go — Autorisation d'Exploitation (AE)
|
||||
//
|
||||
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
|
||||
// They are NOT a separate MongoDB collection — each resource document carries
|
||||
// its own AEs, just like it carries its Instances.
|
||||
//
|
||||
// # Visibility filtering
|
||||
//
|
||||
// When a resource is returned to a consumer peer the AE list is filtered:
|
||||
// - AllowedPeerIDs empty → public AE, visible to all peers.
|
||||
// - AllowedPeerIDs non-empty, contains requester → visible to that peer.
|
||||
// - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
|
||||
//
|
||||
// The resource owner always sees all of their own AEs unfiltered.
|
||||
//
|
||||
// # Enforcement
|
||||
//
|
||||
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
|
||||
// its own package to avoid circular imports) before launching any execution.
|
||||
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
|
||||
// and cause the execution to be rejected.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// CouplingConstraint defines which resources must or must not co-exist in the
|
||||
// same workflow when the protected resource is included.
|
||||
type CouplingConstraint struct {
|
||||
// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
|
||||
// workflow alongside the protected resource.
|
||||
RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
|
||||
// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
|
||||
// workflow alongside the protected resource.
|
||||
ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
|
||||
}
|
||||
|
||||
// ExploitationAuthorization (AE) is embedded in a resource and restricts how
|
||||
// the resource may be used by other consumer peers.
|
||||
//
|
||||
// It is stored as part of the resource document (bson embedded), not as a
|
||||
// separate collection. Create/update it by PATCHing the parent resource.
|
||||
type ExploitationAuthorization struct {
|
||||
// ID is a client-assigned UUID so individual AEs can be referenced.
|
||||
ID string `json:"id" bson:"id"`
|
||||
// Name is a human-readable label shown in the catalog detail view.
|
||||
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
||||
|
||||
// AllowedPeerIDs restricts which consumer peers may use the resource.
|
||||
// An empty list means any peer is allowed.
|
||||
AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`
|
||||
|
||||
// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
|
||||
// An empty list means any workflow is allowed.
|
||||
AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`
|
||||
|
||||
// Coupling describes positive (required) and negative (forbidden) coupling.
|
||||
// Nil means no coupling constraint.
|
||||
Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`
|
||||
|
||||
// ValidFrom / ValidUntil define the active window.
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
|
||||
|
||||
// IsRevoked allows instant revocation without deleting the AE from the resource.
|
||||
IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
|
||||
}
|
||||
|
||||
// IsVisibleTo returns true when this AE should be included in the response to
|
||||
// peerID. The resource owner (creatorID) always sees all AEs.
|
||||
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
|
||||
if peerID == creatorID {
|
||||
return true // owner sees everything
|
||||
}
|
||||
return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
|
||||
}
|
||||
|
||||
// CheckAE evaluates this AE against the execution context and returns any
|
||||
// violations found. workflowResourceIDs is the set of all resource UUIDs in
|
||||
// the workflow; resourceID is the UUID of the resource this AE belongs to.
|
||||
func (ae *ExploitationAuthorization) CheckAE(
|
||||
resourceID, workflowID, consumerPeerID string,
|
||||
workflowResourceIDs map[string]struct{},
|
||||
now time.Time,
|
||||
) []AEViolation {
|
||||
var vs []AEViolation
|
||||
add := func(t AEViolationType, msg string) {
|
||||
vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
|
||||
}
|
||||
|
||||
if ae.IsRevoked {
|
||||
add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
|
||||
return vs
|
||||
}
|
||||
if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
|
||||
add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
|
||||
ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
|
||||
return vs
|
||||
}
|
||||
if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
|
||||
add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
|
||||
ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
|
||||
return vs
|
||||
}
|
||||
if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
|
||||
if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
|
||||
add(AEViolationPeerNotAllowed, fmt.Sprintf(
|
||||
"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
|
||||
}
|
||||
}
|
||||
if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
|
||||
if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
|
||||
add(AEViolationWorkflowNotAllow, fmt.Sprintf(
|
||||
"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
|
||||
}
|
||||
}
|
||||
if ae.Coupling != nil {
|
||||
for _, req := range ae.Coupling.RequiredResourceIDs {
|
||||
if _, ok := workflowResourceIDs[req]; !ok {
|
||||
add(AEViolationCouplingRequired, fmt.Sprintf(
|
||||
"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
|
||||
}
|
||||
}
|
||||
for _, forb := range ae.Coupling.ForbiddenResourceIDs {
|
||||
if _, ok := workflowResourceIDs[forb]; ok {
|
||||
add(AEViolationCouplingForbid, fmt.Sprintf(
|
||||
"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
// ── Violation types ───────────────────────────────────────────────────────────
|
||||
|
||||
type AEViolationType string
|
||||
|
||||
const (
|
||||
AEViolationRevoked AEViolationType = "ae_revoked"
|
||||
AEViolationExpired AEViolationType = "ae_expired"
|
||||
AEViolationNotYetValid AEViolationType = "ae_not_yet_valid"
|
||||
AEViolationPeerNotAllowed AEViolationType = "ae_peer_not_allowed"
|
||||
AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
|
||||
AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
|
||||
AEViolationCouplingForbid AEViolationType = "ae_coupling_forbidden"
|
||||
)
|
||||
|
||||
// AEViolation describes a single constraint that was not satisfied.
|
||||
type AEViolation struct {
|
||||
AEID string
|
||||
ResourceID string
|
||||
Type AEViolationType
|
||||
Message string
|
||||
}
|
||||
|
||||
// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────
|
||||
|
||||
// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
|
||||
// unique AE violation. Call this before rejecting the execution.
|
||||
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
|
||||
if consumerPeerID == "" || len(violations) == 0 {
|
||||
return
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, v := range violations {
|
||||
key := v.AEID + ":" + v.ResourceID
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
report := tools.PeerBehaviorReport{
|
||||
ReporterApp: "oc-scheduler",
|
||||
TargetPeerID: consumerPeerID,
|
||||
Severity: tools.BehaviorFraud,
|
||||
Reason: fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
|
||||
Evidence: v.AEID,
|
||||
At: time.Now().UTC(),
|
||||
}
|
||||
if b, err := json.Marshal(report); err == nil {
|
||||
tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
|
||||
FromApp: "oc-scheduler",
|
||||
Method: int(tools.PEER_BEHAVIOR_EVENT),
|
||||
Payload: b,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OriginType qualifies where a resource instance comes from.
|
||||
type OriginType int
|
||||
|
||||
const (
|
||||
// OriginPeer: instance offered by a known network peer (default).
|
||||
OriginPeer OriginType = iota
|
||||
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
|
||||
// No peer confirmation is needed; access is unrestricted.
|
||||
OriginPublic
|
||||
// OriginSelf: self-hosted instance with no third-party peer.
|
||||
OriginSelf
|
||||
)
|
||||
|
||||
// OriginMeta carries provenance information for a resource instance.
|
||||
type OriginMeta struct {
|
||||
Type OriginType `json:"origin_type" bson:"origin_type"`
|
||||
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
|
||||
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
|
||||
}
|
||||
|
||||
// IsPeerless MUST NOT be used for authorization decisions.
|
||||
// Use ResourceInstance.IsPeerless() instead, which derives the property
|
||||
// from structural invariants rather than this self-declared field.
|
||||
//
|
||||
// This method is kept only for display/logging purposes.
|
||||
func (o OriginMeta) DeclaredPeerless() bool {
|
||||
return o.Type != OriginPeer
|
||||
}
|
||||
@@ -38,7 +38,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
|
||||
}
|
||||
|
||||
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
|
||||
/* EMPTY */
|
||||
// WorkflowResource has no instances, but still carries AEs that must be
|
||||
// filtered before the resource is returned to a non-owner, non-admin peer.
|
||||
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
if request != nil {
|
||||
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
return []ResourceInstanceITF{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package resources
|
||||
|
||||
// OriginType qualifies where a resource instance comes from.
|
||||
type OriginType int
|
||||
|
||||
const (
|
||||
// OriginPeer: instance offered by a known network peer (default).
|
||||
OriginPeer OriginType = iota
|
||||
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
|
||||
// No peer confirmation is needed; access is unrestricted.
|
||||
OriginPublic
|
||||
// OriginSelf: self-hosted instance with no third-party peer.
|
||||
OriginSelf
|
||||
)
|
||||
|
||||
// OriginMeta carries provenance information for a resource instance.
|
||||
type OriginMeta struct {
|
||||
Type OriginType `json:"origin_type" bson:"origin_type"`
|
||||
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
|
||||
License string `json:"origin_license,omitempty" bson:"origin_license,omitempty"` // SPDX identifier or free-form
|
||||
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
|
||||
}
|
||||
|
||||
// IsPeerless MUST NOT be used for authorization decisions.
|
||||
// Use ResourceInstance.IsPeerless() instead, which derives the property
|
||||
// from structural invariants rather than this self-declared field.
|
||||
//
|
||||
// This method is kept only for display/logging purposes.
|
||||
func (o OriginMeta) DeclaredPeerless() bool {
|
||||
return o.Type != OriginPeer
|
||||
}
|
||||
@@ -31,23 +31,23 @@ type ProcessingResource struct {
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
|
||||
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
|
||||
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// IsService marks a long-running processing that acts as a persistent service.
|
||||
// Such processings do not require a Compute booking (they manage their own lifecycle).
|
||||
IsService bool `json:"is_service" bson:"is_service" default:"false"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// License is now in AbstractResource — kept here as alias for backward compat with existing DB docs.
|
||||
// New code should use AbstractResource.License.
|
||||
}
|
||||
|
||||
func (r *ProcessingResource) GetType() string {
|
||||
return tools.PROCESSING_RESOURCE.String()
|
||||
}
|
||||
|
||||
type ProcessingResourceAccess struct {
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
|
||||
}
|
||||
|
||||
type ProcessingInstance struct {
|
||||
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
|
||||
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
}
|
||||
|
||||
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
|
||||
|
||||
@@ -39,6 +39,16 @@ type AbstractResource struct {
|
||||
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
|
||||
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
|
||||
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
|
||||
|
||||
// License is the usage licence of the resource (SPDX identifier or free-text).
|
||||
// Displayed prominently in the catalog detail view.
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
|
||||
// ExploitationAuthorizations (AEs) are coupling and peer-usage constraints
|
||||
// issued by the resource owner. Stored embedded in the resource document,
|
||||
// NOT in a separate collection.
|
||||
// Visibility-filtered per requesting peer before any response is sent.
|
||||
ExploitationAuthorizations []ExploitationAuthorization `json:"exploitation_authorizations,omitempty" bson:"exploitation_authorizations,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType {
|
||||
@@ -83,6 +93,28 @@ func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExploitationAuthorizations returns all AEs attached to this resource.
|
||||
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
|
||||
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
|
||||
return r.ExploitationAuthorizations
|
||||
}
|
||||
|
||||
// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
|
||||
// Must be called before serializing the resource for a consumer peer.
|
||||
// The resource owner (CreatorID) always sees all AEs unfiltered.
|
||||
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
|
||||
if isAdmin {
|
||||
return // admin or owner: no filtering
|
||||
}
|
||||
filtered := r.ExploitationAuthorizations[:0]
|
||||
for _, ae := range r.ExploitationAuthorizations {
|
||||
if ae.IsVisibleTo(peerID, r.CreatorID) {
|
||||
filtered = append(filtered, ae)
|
||||
}
|
||||
}
|
||||
r.ExploitationAuthorizations = filtered
|
||||
}
|
||||
|
||||
func (ri *AbstractResource) ClearEnv() utils.DBObject {
|
||||
ri.Env = []models.Param{}
|
||||
ri.Inputs = []models.Param{}
|
||||
@@ -201,12 +233,15 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
|
||||
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
|
||||
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
|
||||
// Filter AEs: only return AEs visible to the requesting peer.
|
||||
if request != nil {
|
||||
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
inst := []ResourceInstanceITF{}
|
||||
for _, i := range abs.Instances {
|
||||
inst = append(inst, i)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
@@ -528,3 +563,18 @@ func ToResource(
|
||||
}
|
||||
return nil, errors.New("can't found any data resources matching")
|
||||
}
|
||||
|
||||
type ResourceAccess struct {
|
||||
Source *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
|
||||
}
|
||||
|
||||
// HasSource returns true when the access is source-based (no embedded container).
|
||||
func (a *ResourceAccess) HasSource() bool {
|
||||
return a != nil && a.Container == nil && a.Source != nil
|
||||
}
|
||||
|
||||
// HasContainer returns true when an explicit container image is provided.
|
||||
func (a *ResourceAccess) HasContainer() bool {
|
||||
return a != nil && a.Container != nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,13 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
|
||||
}
|
||||
|
||||
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
|
||||
/* EMPTY */
|
||||
// WorkflowResource has no instances, but still carries AEs that must be
|
||||
// filtered before the resource is returned to a non-owner, non-admin peer.
|
||||
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
if request != nil {
|
||||
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
return []ResourceInstanceITF{}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,50 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// DataStorageLink represents a resolved Data→Storage pair found in the graph.
|
||||
type DataStorageLink struct {
|
||||
DataItemID string
|
||||
StorageItemID string
|
||||
}
|
||||
|
||||
// GetDataStorageLinks returns all links that connect a Data item to a Storage item.
|
||||
// These links are mandatory when the Data instance has a Source configured:
|
||||
// the workflow builder uses them to know where to download the data before
|
||||
// any processing step that consumes that storage.
|
||||
func (g *Graph) GetDataStorageLinks() []DataStorageLink {
|
||||
var result []DataStorageLink
|
||||
for _, link := range g.Links {
|
||||
srcItem, srcOk := g.Items[link.Source.ID]
|
||||
dstItem, dstOk := g.Items[link.Destination.ID]
|
||||
if !srcOk || !dstOk {
|
||||
continue
|
||||
}
|
||||
if g.IsData(srcItem) && g.IsStorage(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Source.ID,
|
||||
StorageItemID: link.Destination.ID,
|
||||
})
|
||||
} else if g.IsStorage(srcItem) && g.IsData(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Destination.ID,
|
||||
StorageItemID: link.Source.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLinkedStorageForData returns the storage item IDs linked to a given Data item.
|
||||
func (g *Graph) GetLinkedStorageForData(dataItemID string) []string {
|
||||
var storageIDs []string
|
||||
for _, dsl := range g.GetDataStorageLinks() {
|
||||
if dsl.DataItemID == dataItemID {
|
||||
storageIDs = append(storageIDs, dsl.StorageItemID)
|
||||
}
|
||||
}
|
||||
return storageIDs
|
||||
}
|
||||
|
||||
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
|
||||
if item, ok := g.Items[id]; ok {
|
||||
if item.NativeTool != nil {
|
||||
|
||||
@@ -15,32 +15,32 @@ type GraphItem struct {
|
||||
}
|
||||
|
||||
func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) {
|
||||
if g.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.Data
|
||||
} else if g.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.Compute
|
||||
} else if g.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.Workflow
|
||||
} else if g.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.Processing
|
||||
} else if g.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.Storage
|
||||
} else if g.NativeTool != nil {
|
||||
return tools.NATIVE_TOOL, g.NativeTool
|
||||
} else if g.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, g.Service
|
||||
} else if g.Dynamic != nil {
|
||||
return tools.DYNAMIC_RESOURCE, g.Dynamic
|
||||
if g.ItemResource.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.ItemResource.Data
|
||||
} else if g.ItemResource.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.ItemResource.Compute
|
||||
} else if g.ItemResource.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.ItemResource.Workflow
|
||||
} else if g.ItemResource.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.ItemResource.Processing
|
||||
} else if g.ItemResource.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.ItemResource.Storage
|
||||
} else if g.ItemResource.NativeTool != nil {
|
||||
return tools.NATIVE_TOOL, g.ItemResource.NativeTool
|
||||
} else if g.ItemResource.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, g.ItemResource.Service
|
||||
} else if g.ItemResource.Dynamic != nil {
|
||||
return tools.DYNAMIC_RESOURCE, g.ItemResource.Dynamic
|
||||
}
|
||||
return tools.INVALID, nil
|
||||
}
|
||||
|
||||
func (g *GraphItem) Clear() {
|
||||
g.Data = nil
|
||||
g.Compute = nil
|
||||
g.Workflow = nil
|
||||
g.Processing = nil
|
||||
g.Storage = nil
|
||||
g.Service = nil
|
||||
g.Dynamic = nil
|
||||
g.ItemResource.Data = nil
|
||||
g.ItemResource.Compute = nil
|
||||
g.ItemResource.Workflow = nil
|
||||
g.ItemResource.Processing = nil
|
||||
g.ItemResource.Storage = nil
|
||||
g.ItemResource.Service = nil
|
||||
g.ItemResource.Dynamic = nil
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ func (l *GraphLink) IsComputeLink(g Graph) (bool, string) {
|
||||
if g.Items == nil {
|
||||
return false, ""
|
||||
}
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
+18
-18
@@ -104,17 +104,17 @@ func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
|
||||
|
||||
func plantUMLPrefix(item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.NativeTool != nil:
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
return "e"
|
||||
case item.Data != nil:
|
||||
case item.ItemResource.Data != nil:
|
||||
return "d"
|
||||
case item.Processing != nil:
|
||||
case item.ItemResource.Processing != nil:
|
||||
return "p"
|
||||
case item.Storage != nil:
|
||||
case item.ItemResource.Storage != nil:
|
||||
return "s"
|
||||
case item.Compute != nil:
|
||||
case item.ItemResource.Compute != nil:
|
||||
return "c"
|
||||
case item.Workflow != nil:
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return "wf"
|
||||
}
|
||||
return "u"
|
||||
@@ -123,24 +123,24 @@ func plantUMLPrefix(item graph.GraphItem) string {
|
||||
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
|
||||
func plantUMLItemLine(varName string, item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.NativeTool != nil:
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
// WorkflowEvent has no instance and no configurable attributes.
|
||||
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.NativeTool.GetName())
|
||||
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.ItemResource.NativeTool.GetName())
|
||||
|
||||
case item.Data != nil:
|
||||
return plantUMLResourceLine("Data", varName, item.Data)
|
||||
case item.ItemResource.Data != nil:
|
||||
return plantUMLResourceLine("Data", varName, item.ItemResource.Data)
|
||||
|
||||
case item.Processing != nil:
|
||||
return plantUMLResourceLine("Processing", varName, item.Processing)
|
||||
case item.ItemResource.Processing != nil:
|
||||
return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)
|
||||
|
||||
case item.Storage != nil:
|
||||
return plantUMLResourceLine("Storage", varName, item.Storage)
|
||||
case item.ItemResource.Storage != nil:
|
||||
return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)
|
||||
|
||||
case item.Compute != nil:
|
||||
return plantUMLResourceLine("ComputeUnit", varName, item.Compute)
|
||||
case item.ItemResource.Compute != nil:
|
||||
return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)
|
||||
|
||||
case item.Workflow != nil:
|
||||
return plantUMLResourceLine("Workflow", varName, item.Workflow)
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -911,3 +912,425 @@ func plan[T resources.ResourceInterface](
|
||||
}
|
||||
return resources, priceds, nil
|
||||
}
|
||||
|
||||
// ── Integrity validation ─────────────────────────────────────────────────────
|
||||
|
||||
// Arrow direction constants matching the flutter_flow_chart ArrowDirection enum
|
||||
// (index order: forward=0, backward=1, bidirectionnal=2).
|
||||
const (
|
||||
arrowDirectionBackward int64 = 1
|
||||
)
|
||||
|
||||
// ViolationSeverity distinguishes blocking errors from non-blocking warnings.
|
||||
type ViolationSeverity int
|
||||
|
||||
const (
|
||||
SeverityError ViolationSeverity = iota // Blocks scheduling — must be fixed.
|
||||
SeverityWarning // Reported but non-blocking.
|
||||
)
|
||||
|
||||
// ViolationType identifies the category of the violation.
|
||||
// Mirrors the TopologyErrorType / TopologyWarningType enums in oc-front.
|
||||
type ViolationType string
|
||||
|
||||
const (
|
||||
// Errors — block scheduling
|
||||
ViolationVariableNotFound ViolationType = "variable_not_found"
|
||||
ViolationMissingComputeUnit ViolationType = "missing_compute_unit"
|
||||
ViolationCycle ViolationType = "cycle"
|
||||
ViolationMissingDataStorage ViolationType = "missing_data_storage"
|
||||
|
||||
// Warnings — non-blocking, reported for UX
|
||||
ViolationInvertedArrow ViolationType = "inverted_arrow"
|
||||
ViolationIsolatedProcessing ViolationType = "isolated_processing"
|
||||
ViolationStorageNotLinkedToProcessing ViolationType = "storage_not_linked_to_processing"
|
||||
)
|
||||
|
||||
// IntegrityViolation describes a single structural or semantic problem
|
||||
// found in the workflow graph.
|
||||
type IntegrityViolation struct {
|
||||
Severity ViolationSeverity
|
||||
Type ViolationType
|
||||
ItemIDs []string // graph item IDs involved in the violation
|
||||
Message string
|
||||
}
|
||||
|
||||
func (v IntegrityViolation) IsError() bool { return v.Severity == SeverityError }
|
||||
func (v IntegrityViolation) IsWarning() bool { return v.Severity == SeverityWarning }
|
||||
|
||||
// ValidateIntegrity checks the structural and semantic integrity of the workflow
|
||||
// graph. It must be called by both oc-front (UX enforcement) and oc-schedulerd
|
||||
// (sovereign enforcement, regardless of submission source — the front can be
|
||||
// bypassed via direct API calls).
|
||||
//
|
||||
// Errors (block scheduling):
|
||||
// 1. Variable not found — an arg references $varName not defined in env/inputs.
|
||||
// 2. Missing compute — a Processing/non-HOSTED Service has no Compute linked.
|
||||
// 3. Cycle — the processing DAG contains a directed cycle.
|
||||
// 4. Missing data storage — a Data with Source has no Storage linked.
|
||||
//
|
||||
// Warnings (non-blocking):
|
||||
// 5. Inverted arrow — a backward link between two processing nodes.
|
||||
// 6. Isolated processing — a processing node with no processing neighbours.
|
||||
// 7. Storage not linked to processing — a storage node orphaned from any processing.
|
||||
func (w *Workflow) ValidateIntegrity() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
violations = append(violations, w.validateVariables()...)
|
||||
violations = append(violations, w.validateComputeLinks()...)
|
||||
violations = append(violations, w.detectCycles()...)
|
||||
violations = append(violations, w.validateDataStorageLinks()...)
|
||||
violations = append(violations, w.detectInvertedArrows()...)
|
||||
violations = append(violations, w.detectIsolatedProcessings()...)
|
||||
violations = append(violations, w.detectOrphanedStorages()...)
|
||||
return violations
|
||||
}
|
||||
|
||||
// HasCriticalViolations returns true when ValidateIntegrity found at least one Error.
|
||||
// oc-schedulerd uses this to reject a workflow without inspecting each violation.
|
||||
func (w *Workflow) HasCriticalViolations() bool {
|
||||
for _, v := range w.ValidateIntegrity() {
|
||||
if v.IsError() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// itemName returns a human-readable name for a graph item (falls back to itemID).
|
||||
func (w *Workflow) itemName(itemID string) string {
|
||||
item, ok := w.Graph.Items[itemID]
|
||||
if !ok {
|
||||
return itemID
|
||||
}
|
||||
_, res := item.GetResource()
|
||||
if res != nil {
|
||||
return res.GetName()
|
||||
}
|
||||
return itemID
|
||||
}
|
||||
|
||||
// validateVariables checks that every $varName reference inside w.Args is
|
||||
// defined in the corresponding element's env or inputs — mirroring
|
||||
// WorkflowFactory.validateArgs() in oc-front.
|
||||
var varRefPattern = regexp.MustCompile(`\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?`)
|
||||
|
||||
func (w *Workflow) validateVariables() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
for itemID, argList := range w.Args {
|
||||
if len(argList) == 0 {
|
||||
continue
|
||||
}
|
||||
available := map[string]struct{}{}
|
||||
for _, p := range w.Env[itemID] {
|
||||
if p.Name != "" {
|
||||
available[p.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, p := range w.Inputs[itemID] {
|
||||
if p.Name != "" {
|
||||
available[p.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
name := w.itemName(itemID)
|
||||
for _, arg := range argList {
|
||||
for _, m := range varRefPattern.FindAllStringSubmatch(arg, -1) {
|
||||
varName := m[1]
|
||||
if _, ok := available[varName]; !ok {
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityError,
|
||||
Type: ViolationVariableNotFound,
|
||||
ItemIDs: []string{itemID},
|
||||
Message: fmt.Sprintf(`"%s": arg "%s" → variable $%s is not defined in env or inputs`, name, arg, varName),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// validateComputeLinks checks that every Processing node (and every non-HOSTED
|
||||
// Service node) has at least one Compute linked — mirroring the computeErrors
|
||||
// block in oc-front's checkTopology().
|
||||
func (w *Workflow) validateComputeLinks() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
for id, item := range w.Graph.Items {
|
||||
needsCompute := false
|
||||
var name string
|
||||
switch {
|
||||
case w.Graph.IsProcessing(item) && item.Processing != nil:
|
||||
// IsService processings are long-running services and don't need a Compute booking.
|
||||
if item.Processing.IsService {
|
||||
continue
|
||||
}
|
||||
needsCompute = true
|
||||
name = item.Processing.GetName()
|
||||
case w.Graph.IsService(item) && item.Service != nil:
|
||||
// HOSTED services use an existing endpoint — no Compute booking needed.
|
||||
inst := item.Service.GetSelectedInstance(nil)
|
||||
if inst != nil {
|
||||
if si, ok := inst.(*resources.ServiceInstance); ok && si.Mode == resources.HOSTED {
|
||||
continue
|
||||
}
|
||||
}
|
||||
needsCompute = true
|
||||
name = item.Service.GetName()
|
||||
}
|
||||
if !needsCompute {
|
||||
continue
|
||||
}
|
||||
hasCompute := false
|
||||
for _, link := range w.Graph.Links {
|
||||
var otherID string
|
||||
if link.Source.ID == id {
|
||||
otherID = link.Destination.ID
|
||||
} else if link.Destination.ID == id {
|
||||
otherID = link.Source.ID
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if other, ok := w.Graph.Items[otherID]; ok && w.Graph.IsCompute(other) {
|
||||
hasCompute = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasCompute {
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityError,
|
||||
Type: ViolationMissingComputeUnit,
|
||||
ItemIDs: []string{id},
|
||||
Message: fmt.Sprintf(`"%s" has no compute unit linked`, name),
|
||||
})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// detectCycles runs DFS colouring on the processing→processing directed graph
|
||||
// and reports any back-edge as a cycle error — mirroring dfsCycle() in oc-front.
|
||||
func (w *Workflow) detectCycles() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
|
||||
// Collect processing + service + event node IDs (execution flux nodes).
|
||||
procIDs := map[string]struct{}{}
|
||||
for id, item := range w.Graph.Items {
|
||||
if w.Graph.IsProcessing(item) || w.Graph.IsService(item) || w.Graph.IsNativeTool(item) {
|
||||
procIDs[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Build directed successors honoring ArrowDirection.
|
||||
successors := map[string][]string{}
|
||||
for id := range procIDs {
|
||||
successors[id] = []string{}
|
||||
}
|
||||
for _, link := range w.Graph.Links {
|
||||
src, dst := link.Source.ID, link.Destination.ID
|
||||
_, srcIsProc := procIDs[src]
|
||||
_, dstIsProc := procIDs[dst]
|
||||
if !srcIsProc || !dstIsProc {
|
||||
continue
|
||||
}
|
||||
dir := int64(0)
|
||||
if link.Style != nil {
|
||||
dir = link.Style.ArrowDirection
|
||||
}
|
||||
if dir == arrowDirectionBackward {
|
||||
// Visual arrow reversed: dst runs before src.
|
||||
successors[dst] = append(successors[dst], src)
|
||||
} else {
|
||||
successors[src] = append(successors[src], dst)
|
||||
}
|
||||
}
|
||||
|
||||
// DFS colouring: 0=white, 1=grey (in stack), 2=black (done).
|
||||
color := map[string]int{}
|
||||
reported := map[string]struct{}{}
|
||||
|
||||
var dfs func(u string)
|
||||
dfs = func(u string) {
|
||||
color[u] = 1
|
||||
for _, v := range successors[u] {
|
||||
if color[v] == 1 {
|
||||
key := u + "→" + v
|
||||
if _, seen := reported[key]; !seen {
|
||||
reported[key] = struct{}{}
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityError,
|
||||
Type: ViolationCycle,
|
||||
ItemIDs: []string{u, v},
|
||||
Message: fmt.Sprintf(`Infinite loop: "%s" → "%s" creates a cycle that would block execution indefinitely`,
|
||||
w.itemName(u), w.itemName(v)),
|
||||
})
|
||||
}
|
||||
} else if color[v] == 0 {
|
||||
dfs(v)
|
||||
}
|
||||
}
|
||||
color[u] = 2
|
||||
}
|
||||
for id := range procIDs {
|
||||
if color[id] == 0 {
|
||||
dfs(id)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// validateDataStorageLinks checks that every Data item with a non-empty Source
|
||||
// has at least one Storage linked — the builder needs this to inject the
|
||||
// download step (curl or NATS/Minio protocol).
|
||||
func (w *Workflow) validateDataStorageLinks() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
dataStorageLinks := w.Graph.GetDataStorageLinks()
|
||||
linkedStorage := map[string]struct{}{}
|
||||
for _, dsl := range dataStorageLinks {
|
||||
linkedStorage[dsl.DataItemID] = struct{}{}
|
||||
}
|
||||
for id, item := range w.Graph.Items {
|
||||
if !w.Graph.IsData(item) || item.Data == nil {
|
||||
continue
|
||||
}
|
||||
hasSource := false
|
||||
for _, inst := range item.Data.Instances {
|
||||
if inst.Access.HasSource() {
|
||||
hasSource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSource {
|
||||
continue
|
||||
}
|
||||
if _, ok := linkedStorage[id]; !ok {
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityError,
|
||||
Type: ViolationMissingDataStorage,
|
||||
ItemIDs: []string{id},
|
||||
Message: fmt.Sprintf(`data "%s" has a source but no Storage linked`, item.Data.GetName()),
|
||||
})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// detectInvertedArrows warns when a link between two processing nodes uses a
|
||||
// backward arrow direction — mirroring the invertedArrow warning in oc-front.
|
||||
func (w *Workflow) detectInvertedArrows() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
for _, link := range w.Graph.Links {
|
||||
if link.Style == nil || link.Style.ArrowDirection != arrowDirectionBackward {
|
||||
continue
|
||||
}
|
||||
srcItem, srcOK := w.Graph.Items[link.Source.ID]
|
||||
dstItem, dstOK := w.Graph.Items[link.Destination.ID]
|
||||
if !srcOK || !dstOK {
|
||||
continue
|
||||
}
|
||||
if (w.Graph.IsProcessing(srcItem) || w.Graph.IsService(srcItem)) &&
|
||||
(w.Graph.IsProcessing(dstItem) || w.Graph.IsService(dstItem)) {
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityWarning,
|
||||
Type: ViolationInvertedArrow,
|
||||
ItemIDs: []string{link.Source.ID, link.Destination.ID},
|
||||
Message: fmt.Sprintf(`Reversed arrow between "%s" & "%s": "%s" will execute before "%s" unexpectedly`,
|
||||
w.itemName(link.Destination.ID), w.itemName(link.Source.ID),
|
||||
w.itemName(link.Destination.ID), w.itemName(link.Source.ID)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// detectIsolatedProcessings warns when a processing node has no link to another
|
||||
// processing node — it will execute synchronously with the workflow's first elements.
|
||||
func (w *Workflow) detectIsolatedProcessings() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
procIDs := map[string]struct{}{}
|
||||
for id, item := range w.Graph.Items {
|
||||
if w.Graph.IsProcessing(item) || w.Graph.IsService(item) || w.Graph.IsNativeTool(item) {
|
||||
procIDs[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
for id := range procIDs {
|
||||
hasProcNeighbour := false
|
||||
for _, link := range w.Graph.Links {
|
||||
var otherID string
|
||||
if link.Source.ID == id {
|
||||
otherID = link.Destination.ID
|
||||
} else if link.Destination.ID == id {
|
||||
otherID = link.Source.ID
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if _, ok := procIDs[otherID]; ok {
|
||||
hasProcNeighbour = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasProcNeighbour {
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityWarning,
|
||||
Type: ViolationIsolatedProcessing,
|
||||
ItemIDs: []string{id},
|
||||
Message: fmt.Sprintf(`"%s" is isolated (no connection with another processing) — will execute synchronously with the workflow's first element(s)`,
|
||||
w.itemName(id)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// detectOrphanedStorages warns when a storage node is not linked to any
|
||||
// processing node — it contributes no data flow to the workflow.
|
||||
func (w *Workflow) detectOrphanedStorages() []IntegrityViolation {
|
||||
var violations []IntegrityViolation
|
||||
for id, item := range w.Graph.Items {
|
||||
if !w.Graph.IsStorage(item) {
|
||||
continue
|
||||
}
|
||||
linkedTopics := map[string]struct{}{}
|
||||
for _, link := range w.Graph.Links {
|
||||
var otherID string
|
||||
if link.Source.ID == id {
|
||||
otherID = link.Destination.ID
|
||||
} else if link.Destination.ID == id {
|
||||
otherID = link.Source.ID
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if other, ok := w.Graph.Items[otherID]; ok {
|
||||
switch {
|
||||
case w.Graph.IsProcessing(other):
|
||||
linkedTopics["processing"] = struct{}{}
|
||||
case w.Graph.IsCompute(other):
|
||||
linkedTopics["compute"] = struct{}{}
|
||||
case w.Graph.IsData(other):
|
||||
linkedTopics["data"] = struct{}{}
|
||||
case w.Graph.IsService(other):
|
||||
linkedTopics["service"] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := linkedTopics["processing"]; ok {
|
||||
continue
|
||||
}
|
||||
name := w.itemName(id)
|
||||
var msg string
|
||||
if len(linkedTopics) == 0 {
|
||||
msg = fmt.Sprintf(`"%s" is isolated (not linked to anything)`, name)
|
||||
} else {
|
||||
topics := make([]string, 0, len(linkedTopics))
|
||||
for t := range linkedTopics {
|
||||
topics = append(topics, t)
|
||||
}
|
||||
msg = fmt.Sprintf(`"%s" is not linked to any processing (only linked to: %s)`, name, strings.Join(topics, ", "))
|
||||
}
|
||||
violations = append(violations, IntegrityViolation{
|
||||
Severity: SeverityWarning,
|
||||
Type: ViolationStorageNotLinkedToProcessing,
|
||||
ItemIDs: []string{id},
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
// - State : current lifecycle state of the step
|
||||
// - Deps : itemIDs that must reach StepSuccess before this step can start
|
||||
// - WhenRunning : itemIDs (resources) that become active while this step is running
|
||||
// (e.g. the compute node executing it, the storage it reads/writes)
|
||||
// (e.g. the compute node executing it, the storage it reads/writes)
|
||||
type ExecutionGraphItem struct {
|
||||
Name string `json:"name" bson:"name"`
|
||||
StartDate *time.Time `json:"start_date,omitempty" bson:"start_date,omitempty"`
|
||||
@@ -69,10 +69,10 @@ func BuildExecutionGraph(g *workflowgraph.Graph) ExecutionGraph {
|
||||
|
||||
// Steps (logical nodes that sequence execution): Data, Processing, Workflow, NativeTool.
|
||||
// Resources (infrastructure co-active while a step runs): Compute, Storage.
|
||||
srcIsStep := srcItem.Data != nil || srcItem.Processing != nil || srcItem.Workflow != nil || srcItem.NativeTool != nil
|
||||
dstIsStep := dstItem.Data != nil || dstItem.Processing != nil || dstItem.Workflow != nil || dstItem.NativeTool != nil
|
||||
srcIsResource := srcItem.Compute != nil || srcItem.Storage != nil
|
||||
dstIsResource := dstItem.Compute != nil || dstItem.Storage != nil
|
||||
srcIsStep := srcItem.ItemResource.Data != nil || srcItem.ItemResource.Processing != nil || srcItem.ItemResource.Workflow != nil || srcItem.ItemResource.NativeTool != nil
|
||||
dstIsStep := dstItem.ItemResource.Data != nil || dstItem.ItemResource.Processing != nil || dstItem.ItemResource.Workflow != nil || dstItem.ItemResource.NativeTool != nil
|
||||
srcIsResource := srcItem.ItemResource.Compute != nil || srcItem.ItemResource.Storage != nil
|
||||
dstIsResource := dstItem.ItemResource.Compute != nil || dstItem.ItemResource.Storage != nil
|
||||
|
||||
switch {
|
||||
case srcIsStep && dstIsStep:
|
||||
|
||||
@@ -57,6 +57,12 @@ type WorkflowExecution struct {
|
||||
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
|
||||
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
|
||||
SelectedStrategies workflow.ConfigItem `json:"selected_strategies"`
|
||||
SelectedPaymentMode workflow.ConfigItem `json:"selected_payment_mode"`
|
||||
|
||||
// SelectedBillingStrategy est la fréquence de facturation globale choisie par l'utilisateur
|
||||
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
|
||||
// Propagée depuis WorkflowSchedule.SelectedBillingStrategy par GenerateExecutions().
|
||||
SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy" bson:"selected_billing_strategy"`
|
||||
|
||||
// SelectedEmbeddedStorages records which storage capability was activated on
|
||||
// each compute unit graph node (key = compute graph node ID).
|
||||
@@ -275,6 +281,16 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
|
||||
if len(executionsID) > 8 {
|
||||
name += " " + executionsID[:8]
|
||||
}
|
||||
// Résout le mode de paiement spécifique à cette ressource depuis SelectedPaymentMode.
|
||||
// SelectedPaymentMode est un ConfigItem (map[string]int) dont les clés sont les IDs
|
||||
// de nœuds du graph ; on tente itemID puis l'ID de la ressource comme fallback.
|
||||
paymentType := pricing.PAY_ONCE
|
||||
if v, ok := d.SelectedPaymentMode[itemID]; ok {
|
||||
paymentType = pricing.PaymentType(v)
|
||||
} else if v, ok := d.SelectedPaymentMode[priced.GetID()]; ok {
|
||||
paymentType = pricing.PaymentType(v)
|
||||
}
|
||||
|
||||
bookingItem := &booking.Booking{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
UUID: uuid.New().String(),
|
||||
@@ -293,6 +309,8 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
|
||||
ExecutionID: d.GetID(),
|
||||
ExpectedStartDate: start,
|
||||
ExpectedEndDate: endDate,
|
||||
BillingStrategy: d.SelectedBillingStrategy,
|
||||
PaymentType: paymentType,
|
||||
}
|
||||
items = append(items, bookingItem)
|
||||
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(
|
||||
|
||||
+21
-1
@@ -36,6 +36,10 @@ const (
|
||||
SERVICE_RESOURCE
|
||||
DYNAMIC_RESOURCE
|
||||
LIVE_SERVICE
|
||||
PAYMENT
|
||||
REFUND
|
||||
DISCOUNT
|
||||
SUBSCRIPTION
|
||||
)
|
||||
|
||||
var NOAPI = func() string {
|
||||
@@ -96,6 +100,10 @@ var InnerDefaultAPI = [...]func() string{
|
||||
CATALOGAPI,
|
||||
CATALOGAPI,
|
||||
DATACENTERAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
}
|
||||
|
||||
// Bind the standard data name to the data type
|
||||
@@ -126,6 +134,10 @@ var Str = [...]string{
|
||||
"service_resource",
|
||||
"dynamic_resource",
|
||||
"live_service",
|
||||
"payment",
|
||||
"refund",
|
||||
"discount",
|
||||
"subscription",
|
||||
}
|
||||
|
||||
func FromString(comp string) int {
|
||||
@@ -161,7 +173,8 @@ func DataTypeList() []DataType {
|
||||
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
|
||||
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
|
||||
ORDER, PURCHASE_RESOURCE,
|
||||
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL, EXECUTION_VERIFICATION, ALLOWED_IMAGE, SERVICE_RESOURCE, DYNAMIC_RESOURCE, LIVE_SERVICE}
|
||||
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL, EXECUTION_VERIFICATION, ALLOWED_IMAGE, SERVICE_RESOURCE, DYNAMIC_RESOURCE, LIVE_SERVICE,
|
||||
PAYMENT, REFUND, DISCOUNT, SUBSCRIPTION}
|
||||
}
|
||||
|
||||
type PropalgationMessage struct {
|
||||
@@ -191,6 +204,10 @@ const (
|
||||
// PB_PROPAGATE is used by oc-discovery to broadcast a peer's online/offline
|
||||
// state to other oc-discovery nodes in the federation via PROPALGATION_EVENT.
|
||||
PB_PROPAGATE
|
||||
// PB_SOURCE_PRESIGN is sent by oc-datacenter to request a pre-signed Minio URL
|
||||
// for a private source resource (isReachable=false, Phase 4).
|
||||
// oc-discovery routes it to the resource owner peer via ProtocolSourcePresignResource.
|
||||
PB_SOURCE_PRESIGN
|
||||
)
|
||||
|
||||
func GetActionString(ss string) PubSubAction {
|
||||
@@ -223,6 +240,8 @@ func GetActionString(ss string) PubSubAction {
|
||||
return PB_OBSERVE_CLOSE
|
||||
case "propagate":
|
||||
return PB_PROPAGATE
|
||||
case "source_presign":
|
||||
return PB_SOURCE_PRESIGN
|
||||
default:
|
||||
return NONE
|
||||
}
|
||||
@@ -246,6 +265,7 @@ var path = []string{
|
||||
"observe", // 13 PB_OBSERVE
|
||||
"observe_close", // 14 PB_OBSERVE_CLOSE
|
||||
"propagate", // 15 PB_PROPAGATE
|
||||
"source_presign", // 16 PB_SOURCE_PRESIGN
|
||||
}
|
||||
|
||||
func (m PubSubAction) String() string {
|
||||
|
||||
@@ -599,6 +599,16 @@ func (k *KubernetesService) CreateSecret(context context.Context, minioId string
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSecretsByLabel deletes all Secrets in the given namespace matching labelSelector.
|
||||
// Used by oc-datacenter to clean up ephemeral source-presigned Secrets after workflow completion.
|
||||
func (k *KubernetesService) DeleteSecretsByLabel(ctx context.Context, namespace, labelSelector string) error {
|
||||
return k.Set.CoreV1().Secrets(namespace).DeleteCollection(
|
||||
ctx,
|
||||
metav1.DeleteOptions{},
|
||||
metav1.ListOptions{LabelSelector: labelSelector},
|
||||
)
|
||||
}
|
||||
|
||||
// CreatePVC creates a static PersistentVolume + PersistentVolumeClaim in the given namespace.
|
||||
// Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock
|
||||
// with Admiralty virtual nodes — the PVC binds immediately.
|
||||
|
||||
@@ -32,6 +32,7 @@ var meths = []string{"remove execution", "create execution", "planner execution"
|
||||
"considers event", "admiralty config event", "minio config event", "pvc config event",
|
||||
"workflow started event", "workflow step done event", "workflow done event",
|
||||
"peer behavior event", "peer observe response event", "peer observe event",
|
||||
"source presign event",
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -78,6 +79,12 @@ const (
|
||||
// or stop observing a remote peer. Payload contains the target peer_id and a
|
||||
// boolean close flag.
|
||||
PEER_OBSERVE_EVENT
|
||||
|
||||
// SOURCE_PRESIGN_EVENT is emitted by oc-discovery on the resource-owner peer
|
||||
// when it receives a PB_SOURCE_PRESIGN request routed via libp2p.
|
||||
// oc-datacenter listens to it to generate a pre-signed Minio URL and reply
|
||||
// via PB_CONSIDERS (Phase 4 — isReachable=false).
|
||||
SOURCE_PRESIGN_EVENT
|
||||
)
|
||||
|
||||
func (n NATSMethod) String() string {
|
||||
@@ -90,7 +97,8 @@ func NameToMethod(name string) NATSMethod {
|
||||
CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT,
|
||||
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT, PVC_CONFIG_EVENT,
|
||||
WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT,
|
||||
PEER_BEHAVIOR_EVENT, PEER_OBSERVE_RESPONSE_EVENT, PEER_OBSERVE_EVENT} {
|
||||
PEER_BEHAVIOR_EVENT, PEER_OBSERVE_RESPONSE_EVENT, PEER_OBSERVE_EVENT,
|
||||
SOURCE_PRESIGN_EVENT} {
|
||||
if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) {
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user