// charge an object. Deduct from an object, create one or more objects and // associated to one or more wallets func (c *ObjectController) Charge(params martini.Params, res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // parse body var body objectChargeBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_body", "request body is invalid or malformed. Expects valid json body") return } // TODO: get client id from access token clientID := "kl14zFDq4SHlmmmVNHgLtE0LqCo8BTjyShOH" // get db transaction object dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // get service service, _, _ := models.FindServiceByClientId(dbTx, clientID) // ensure object ids is not empty if len(body.IDS) == 0 { services.Res(res).ErrParam("ids").Error(400, "invalid_parameter", "provide one or more object ids to charge") return } // ensure object ids length is less than 100 if len(body.IDS) > 100 { services.Res(res).ErrParam("ids").Error(400, "invalid_parameter", "only a maximum of 100 objects can be charge at a time") return } // ensure destination wallet is provided if c.validate.IsEmpty(body.DestinationWalletID) { services.Res(res).ErrParam("wallet_id").Error(400, "invalid_parameter", "destination wallet id is reqired") return } // ensure amount is provided if body.Amount < MinimumObjectUnit { services.Res(res).ErrParam("amount").Error(400, "invalid_parameter", fmt.Sprintf("amount is below the minimum charge limit. Mininum charge limit is %.8f", MinimumObjectUnit)) return } // if meta is provided, ensure it is not greater than the limit size if !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { services.Res(res).ErrParam("meta").Error(400, "invalid_parameter", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } // ensure destination wallet exists wallet, found, err := models.FindWalletByObjectID(dbTx, body.DestinationWalletID) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "api_error") return } else if !found { dbTx.Rollback() services.Res(res).ErrParam("wallet_id").Error(404, "not_found", "wallet_id not found") return } // find all objects objectsFound, err := models.FindAllObjectsByObjectID(dbTx, body.IDS) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "api_error") return } // ensure all objects exists if len(objectsFound) != len(body.IDS) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(404, "object_error", "one or more objects do not exist") return } // sort object by balance in descending order sort.Sort(services.ByObjectBalance(objectsFound)) // objects to charge objectsToCharge := []models.Object{} // validate each object // check open status (timed and pin) // collect the required objects to sufficiently // complete a charge from the list of found objects for _, object := range objectsFound { // as long as the total balance of objects to be charged is not above charge amount // keep setting aside objects to charge from. // once we have the required objects to cover charge amount, stop processing other objects if TotalBalance(objectsToCharge) < body.Amount { objectsToCharge = append(objectsToCharge, object) } else { break } // ensure service is the issuer of object if object.Service.ObjectID != service.ObjectID { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: service cannot charge an object not issued by it", object.ObjectID)) return } // ensure object is open if !object.Open { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object is not opened and cannot be charged", object.ObjectID)) return } else { // for object with open_timed open method, ensure time is not passed if object.OpenMethod == models.ObjectOpenTimed { objectOpenTime := services.UnixToTime(object.OpenTime).UTC() now := time.Now().UTC() if now.After(objectOpenTime) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object open time period has expired", object.ObjectID)) return } } // for object with open_pin open method, ensure pin is provided and // it matches. Pin should be found in the optional pin object of the request body if object.OpenMethod == models.ObjectOpenPin { if pin, found := body.Pins[object.ObjectID]; found { // ensure pin provided matches objects pin if !services.BcryptCompare(object.OpenPin, strconv.Itoa(pin)) { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: pin provided to open object is invalid", object.ObjectID)) return } } else { dbTx.Rollback() services.Res(res).ErrParam("ids").Error(402, "object_error", fmt.Sprintf("%s: object pin not found in pin parameter of request body", object.ObjectID)) return } } } } totalObjectsBalance := TotalBalance(objectsToCharge) // ensure total balance of objects to charge is sufficient for charge amount if totalObjectsBalance < body.Amount { dbTx.Rollback() services.Res(res).ErrParam("amount").Error(402, "invalid_parameter", fmt.Sprintf("object%s total balance not sufficient to cover charge amount", services.SIfNotZero(len(body.IDS)))) return } lastObj := objectsToCharge[len(objectsToCharge)-1] // if there is excess, the last object is always the supplement object. // deduct from last object's balance, update object and remove it from the objectsToCharge list if totalObjectsBalance > body.Amount { lastObj.Balance = totalObjectsBalance - body.Amount objectsToCharge = objectsToCharge[0 : len(objectsToCharge)-1] dbTx.Save(&lastObj) } // delete the objects to charge for _, object := range objectsToCharge { dbTx.Delete(&object) } // create new object. set balance to charge balance // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(service.Identity.BaseCurrency)] newPin, err := services.NewObjectPin(strconv.Itoa(countryCallCode)) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "server error") return } newObj := NewObject(newPin, models.ObjectValue, service, wallet, body.Amount, body.Meta) err = models.CreateObject(dbTx, &newObj) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "api_error", "server error") return } dbTx.Save(&newObj).Commit() services.Res(res).Json(newObj) }
// merge two or more objects. // Only a max of 100 identitcal objects can be merged. // All objects to be merged must exists. // Only similar objects can be merged. // Meta is not retained. Optional "meta" parameter can be // provided as new meta for the resulting object // TODO: Needs wallet authorization with scode "obj_merge" func (c *ObjectController) Merge(res http.ResponseWriter, req services.AuxRequestContext, log *config.CustomLog, db *services.DB) { // authorizing wallet id // TODO: get this from the access token authWalletID := "55c679145fe09c74ed000001" // parse body var body objectMergeBody if err := c.ParseJsonBody(req, &body); err != nil { services.Res(res).Error(400, "invalid_body", "request body is invalid or malformed. Expects valid json body") return } // objects field is required if body.Objects == nil { services.Res(res).Error(400, "missing_parameter", "Missing required field: objects") return } // objects field must contain at least two objects if len(body.Objects) < 2 { services.Res(res).Error(400, "invalid_parameter", "objects: minimum of two objects required") return } // objects field must not contain more than 100 objects if len(body.Objects) > 100 { services.Res(res).Error(400, "invalid_parameter", "objects: cannot merge more than 100 objects in a request") return } // ensure objects contain no duplicates if services.StringSliceHasDuplicates(body.Objects) { services.Res(res).Error(400, "invalid_parameter", "objects: must not contain duplicate objects") return } // if meta is provided, ensure it is not greater than the limit size if !c.validate.IsEmpty(body.Meta) && len([]byte(body.Meta)) > MaxMetaSize { services.Res(res).Error(400, "invalid_meta_size", fmt.Sprintf("Meta contains too much data. Max size is %d bytes", MaxMetaSize)) return } // get db transaction object dbTx, err := db.GetPostgresHandleWithRepeatableReadTrans() if err != nil { c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // find all objects objectsFound, err := models.FindAllObjectsByObjectID(dbTx, body.Objects) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } // ensure all objects where found if len(objectsFound) != len(body.Objects) { dbTx.Rollback() services.Res(res).Error(400, "unknown_merge_objects", "one or more objects does not exists") return } totalBalance := 0.0 firstObj := objectsFound[0] checkObjName := firstObj.Service.Identity.ObjectName for _, object := range objectsFound { // ensure all objects are valuable and also ensure object's if object.Type == models.ObjectValueless { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "objects: only valuable objects (object_value) can be merged") return } // wallet id match the authorizing wallet id if object.Wallet.ObjectID != authWalletID { dbTx.Rollback() services.Res(res).Error(401, "unauthorized", "objects: one or more objects belongs to a different wallet") return } // ensure all objects are similar by their name / same issuer. // this also ensures all objects have the same base currency if checkObjName != object.Service.Identity.ObjectName { dbTx.Rollback() services.Res(res).Error(400, "invalid_parameter", "objects: only similar (by name) objects can be merged") return } // updated total balance totalBalance += object.Balance // delete object dbTx.Delete(&object) } // create a new object // generate a pin countryCallCode := config.CurrencyCallCodes[strings.ToUpper(firstObj.Service.Identity.BaseCurrency)] newPin, err := services.NewObjectPin(strconv.Itoa(countryCallCode)) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } newObj := NewObject(newPin, models.ObjectValue, firstObj.Service, firstObj.Wallet, totalBalance, body.Meta) err = models.CreateObject(dbTx, &newObj) if err != nil { dbTx.Rollback() c.log.Error(err.Error()) services.Res(res).Error(500, "", "server error") return } dbTx.Commit() services.Res(res).Json(newObj) }