Example #1
0
// 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)
}
Example #2
0
// 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)
}