Description:
Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
pragma solidity ^0.8.15;
// \
// \
// \
// >\/7
// _.-(6' \
// (=___._/ \
// ) \ |
// / / |
// / > /
// j < _\
// UNISUB IO
//
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
library BokkyPooBahsDateTimeLibrary {
uint constant SECONDS_PER_DAY = 24 * 60 * 60;
uint constant SECONDS_PER_HOUR = 60 * 60;
uint constant SECONDS_PER_MINUTE = 60;
int constant OFFSET19700101 = 2440588;
uint constant DOW_MON = 1;
uint constant DOW_TUE = 2;
uint constant DOW_WED = 3;
uint constant DOW_THU = 4;
uint constant DOW_FRI = 5;
uint constant DOW_SAT = 6;
uint constant DOW_SUN = 7;
// ------------------------------------------------------------------------
// Calculate the number of days from 1970/01/01 to year/month/day using
// the date conversion algorithm from
// http://aa.usno.navy.mil/faq/docs/JD_Formula.php
// and subtracting the offset 2440588 so that 1970/01/01 is day 0
//
// days = day
// - 32075
// + 1461 * (year + 4800 + (month - 14) / 12) / 4
// + 367 * (month - 2 - (month - 14) / 12 * 12) / 12
// - 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4
// - offset
// ------------------------------------------------------------------------
function _daysFromDate(uint year, uint month, uint day) internal pure returns (uint _days) {
require(year >= 1970);
int _year = int(year);
int _month = int(month);
int _day = int(day);
int __days = _day
- 32075
+ 1461 * (_year + 4800 + (_month - 14) / 12) / 4
+ 367 * (_month - 2 - (_month - 14) / 12 * 12) / 12
- 3 * ((_year + 4900 + (_month - 14) / 12) / 100) / 4
- OFFSET19700101;
_days = uint(__days);
}
// ------------------------------------------------------------------------
// Calculate year/month/day from the number of days since 1970/01/01 using
// the date conversion algorithm from
// http://aa.usno.navy.mil/faq/docs/JD_Formula.php
// and adding the offset 2440588 so that 1970/01/01 is day 0
//
// int L = days + 68569 + offset
// int N = 4 * L / 146097
// L = L - (146097 * N + 3) / 4
// year = 4000 * (L + 1) / 1461001
// L = L - 1461 * year / 4 + 31
// month = 80 * L / 2447
// dd = L - 2447 * month / 80
// L = month / 11
// month = month + 2 - 12 * L
// year = 100 * (N - 49) + year + L
// ------------------------------------------------------------------------
function _daysToDate(uint _days) internal pure returns (uint year, uint month, uint day) {
int __days = int(_days);
int L = __days + 68569 + OFFSET19700101;
int N = 4 * L / 146097;
L = L - (146097 * N + 3) / 4;
int _year = 4000 * (L + 1) / 1461001;
L = L - 1461 * _year / 4 + 31;
int _month = 80 * L / 2447;
int _day = L - 2447 * _month / 80;
L = _month / 11;
_month = _month + 2 - 12 * L;
_year = 100 * (N - 49) + _year + L;
year = uint(_year);
month = uint(_month);
day = uint(_day);
}
function _isLeapYear(uint year) internal pure returns (bool leapYear) {
leapYear = ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
function _getDaysInMonth(uint year, uint month) internal pure returns (uint daysInMonth) {
if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) {
daysInMonth = 31;
} else if (month != 2) {
daysInMonth = 30;
} else {
daysInMonth = _isLeapYear(year) ? 29 : 28;
}
}
function addYears(uint timestamp, uint _years) internal pure returns (uint newTimestamp) {
uint year;
uint month;
uint day;
(year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY);
year += _years;
uint daysInMonth = _getDaysInMonth(year, month);
if (day > daysInMonth) {
day = daysInMonth;
}
newTimestamp = _daysFromDate(year, month, day) * SECONDS_PER_DAY + timestamp % SECONDS_PER_DAY;
require(newTimestamp >= timestamp);
}
function addMonths(uint timestamp, uint _months) internal pure returns (uint newTimestamp) {
uint year;
uint month;
uint day;
(year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY);
month += _months;
year += (month - 1) / 12;
month = (month - 1) % 12 + 1;
uint daysInMonth = _getDaysInMonth(year, month);
if (day > daysInMonth) {
day = daysInMonth;
}
newTimestamp = _daysFromDate(year, month, day) * SECONDS_PER_DAY + timestamp % SECONDS_PER_DAY;
require(newTimestamp >= timestamp);
}
function diffYears(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _years) {
require(fromTimestamp <= toTimestamp);
uint fromYear;
uint fromMonth;
uint fromDay;
uint toYear;
uint toMonth;
uint toDay;
(fromYear, fromMonth, fromDay) = _daysToDate(fromTimestamp / SECONDS_PER_DAY);
(toYear, toMonth, toDay) = _daysToDate(toTimestamp / SECONDS_PER_DAY);
_years = toYear - fromYear;
}
function diffMonths(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _months) {
require(fromTimestamp <= toTimestamp);
uint fromYear;
uint fromMonth;
uint fromDay;
uint toYear;
uint toMonth;
uint toDay;
(fromYear, fromMonth, fromDay) = _daysToDate(fromTimestamp / SECONDS_PER_DAY);
(toYear, toMonth, toDay) = _daysToDate(toTimestamp / SECONDS_PER_DAY);
_months = toYear * 12 + toMonth - fromYear * 12 - fromMonth;
}
function diffDays(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _days) {
require(fromTimestamp <= toTimestamp);
_days = (toTimestamp - fromTimestamp) / SECONDS_PER_DAY;
}
function diffHours(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _hours) {
require(fromTimestamp <= toTimestamp);
_hours = (toTimestamp - fromTimestamp) / SECONDS_PER_HOUR;
}
function diffMinutes(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _minutes) {
require(fromTimestamp <= toTimestamp);
_minutes = (toTimestamp - fromTimestamp) / SECONDS_PER_MINUTE;
}
function diffSeconds(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _seconds) {
require(fromTimestamp <= toTimestamp);
_seconds = toTimestamp - fromTimestamp;
}
}
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
contract SubscriptionApp {
bool initialized;
address public owner;
address public operator;
uint256 public defaultPlatformFee;
uint256 public nextOrder;
mapping(bytes32 => address) public customerIdToAddress;
// Events
event OrderCreated(
uint256 orderId,
address merchant,
uint256 chargePerInterval,
uint256 extraBudgetPerInterval,
uint256 startTime,
uint256 intervalDuration,
address erc20,
uint256 merchantDefaultNumberOfOrderIntervals,
uint256 trialIntervals,
string passthrough
);
event OrderAccepted(
uint256 orderId,
bytes32 customerId,
address customerAddress,
uint256 startTime,
uint256 extraBudgetPerInterval,
uint256 approvedPeriodsRemaining,
uint256 trialIntervalsRemaining,
string passthrough
);
event OrderPaidOut(
uint256 orderId,
bytes32 customerId,
uint256 amount,
uint256 feeAmount,
uint256 timestamp,
address executor, // Merchant or owner address that paid out
string passthrough
);
event ExtraBudgetLogged(
uint256 orderId,
bytes32 customerId,
address customerAddress,
uint256 extraAmount,
uint256 pendingPeriods,
uint256 index,
string passthrough
);
event ExtraBudgetPaymentProcessed(
uint256 orderId,
bytes32 customerId,
address customerAddress,
uint256 amount,
uint256 index
);
event ExtraBudgetPaidOut(
uint256 orderId,
uint256 amount,
uint256 startPaymentIndex,
uint256 endPaymentIndex
);
event ExtraBudgetRefunded(
uint256 orderId,
bytes32 customerId,
address customerAddress,
uint256 extraAmount, // Refunded amount
uint256 index
);
event OrderPaidOutGasSavingMode(
uint256 orderId,
bytes32 customerId,
uint256 amount,
uint256 feeAmount,
uint256 timestamp,
address executor, // Merchant or owner address that paid out
string passthrough
);
event OrderRenewed(
uint256 orderId,
bytes32 customerId,
address customerAddress,
uint256 startTime,
uint256 approvedPeriodsRemaining,
bool orderRenewedNotExtended,
string passthrough
);
event OrderCancelled(
uint256 orderId,
bytes32 customerId,
address customerAddress
);
event OrderPaused(
uint256 orderId,
bool isPaused,
address whoPausedIt
);
event OrderSetMerchantDefaultNumberOfOrderIntervals(
uint256 orderId,
uint256 defaultNumberOfOrderIntervals,
address whoSetIt
);
event SuccessfulPay(uint256 orderId,
bytes32 customerId,
address customerAddress,
string passthrough);
event PaymentFailureBytes(bytes someData,
uint256 orderId,
bytes32 customerId,
address customerAddress,
string passthrough);
event PaymentFailure(string revertString,
uint256 orderId,
bytes32 customerId,
address customerAddress,
string passthrough);
event SetMerchantSpecificPlatformFee(address merchant, uint256 customPlatformFee, bool activated);
event SetMerchantSpecificExtraBudgetLockTime(address merchant, uint256 customLockTime);
event MerchantWithdrawERC20(address erc20, address merchant, uint256 value);
event OwnerWithdrawERC20(address erc20, uint256 value);
event ChangeOwner(address newOwner);
event ChangeOperator(address operator);
event ChangeMerchantOperator(address merchant, address merchantOperator);
// Structs
struct CustomerOrder {
bytes32 customerId;
address customerAddress;
uint256 extraBudgetPerInterval;
uint256 extraBudgetUsed;
uint256 extraBudgetLifetime;
uint256 approvedPeriodsRemaining;
uint256 trialIntervalsRemaining;
uint256 firstPaymentMadeTimestamp;
uint256 numberOfIntervalsPaid;
bool terminated;
uint256 amountPaidToDate;
}
struct Order {
uint256 orderId;
address merchant;
uint256 chargePerInterval;
uint256 extraBudgetPerInterval;
uint256 startTime;
uint256 intervalDuration;
address erc20;
bool paused;
uint256 trialIntervals;
uint256 merchantDefaultNumberOfOrderIntervals;
mapping(bytes32 => CustomerOrder) customerOrders;
}
struct PendingExtraBudgetPayment {
bytes32 customerId;
address customerAddress;
uint256 timestamp;
uint256 extraAmount;
bool processed;
bool refunded;
}
// New Struct
struct ExtraBudgetArgs {
uint256 orderId;
bytes32 customerId;
uint256 extraAmount;
uint256 pendingIntervals;
string passthrough;
}
struct ProcessPaymentArgs {
uint256 orderId;
bytes32 customerId;
bool gasSavingMode;
uint256 extraBudgetAmount;
string passthrough;
}
/// @notice order id to order
mapping(uint256 => Order) public orders;
mapping(uint256 => mapping(bytes32=> uint256[])) public customerHistoryTimestamps;
mapping(uint256 => mapping(bytes32=> uint256[])) public customerHistoryAmounts;
mapping(uint256 => mapping(bytes32=> uint256[])) public customerHistoryFeePercentages;
mapping(address => bool) public customPlatformFeeAssigned;
mapping(address => uint256) public customPlatformFee;
mapping(address => uint256) public pendingOwnerWithdrawalAmountByToken;
mapping(address => mapping (address => uint256)) public pendingMerchantWithdrawalAmountByMerchantAndToken;
// Order id -> list of pending extra budget payments
mapping(uint256 => PendingExtraBudgetPayment[]) public pendingExtraBudgetPaymentListByOrderAndCustomer;
uint256 public lockDownPeriodExtraBudgetPayment;
mapping(address => uint256) public customLockDownPeriodExtraBudgetPaymentMerchants;
// Operator upgrade
mapping(address => address) public merchantOperators;
modifier onlyOwner() {
require(owner == msg.sender, "Caller is not the owner");
_;
}
constructor(){
}
function initialize(uint256 _defaultPlatformFee) public{
if(!initialized){
defaultPlatformFee = _defaultPlatformFee;
owner = msg.sender;
operator = address(0);
nextOrder = 0;
lockDownPeriodExtraBudgetPayment = 604800; // 7 days in seconds
initialized = true;
}
}
/// @dev ChangeOwner
/// @param newOwner The new Owner
function changeOwner(address newOwner) public onlyOwner {
require(newOwner != address(0), 'Cannot change owner to 0');
owner = newOwner;
emit ChangeOwner(newOwner);
}
/// @dev ChangeOperator
/// @param _operator The new Operator authorized to process payments
function changeOperator(address _operator) public onlyOwner {
operator = _operator;
emit ChangeOperator(_operator);
}
/// @dev ChangeMerchantOperator
/// @param _merchantOperator The new merchant Operator authorized to process payments
function changeMerchantOperatorOnlyOwner(address _merchant, address _merchantOperator) public onlyOwner {
merchantOperators[_merchant] = _merchantOperator;
emit ChangeMerchantOperator(_merchant, _merchantOperator);
}
/// @dev ChangeMerchantOperator
/// @param _merchantOperator The new merchant Operator authorized to process payments
function changeMerchantOperator(address _merchantOperator) public {
merchantOperators[msg.sender] = _merchantOperator;
emit ChangeMerchantOperator(msg.sender, _merchantOperator);
}
/// @dev ChangeDefaultPlatformFee
/// @param _defaultPlatformFee The new fee for using the platform
function changeDefaultPlatformFee(uint _defaultPlatformFee) public onlyOwner {
defaultPlatformFee = _defaultPlatformFee;
}
/// @dev SetMerchantSpecificPlatformFee
/// @param _merchant The merchant
/// @param _platformFee Custom platform fee for merchant
/// @param _activated Fee activated with custom platform fee or deactivated
function setMerchantSpecificPlatformFee(address _merchant, uint256 _platformFee, bool _activated) public onlyOwner {
if(_activated){
customPlatformFeeAssigned[_merchant] = true;
customPlatformFee[_merchant] = _platformFee;
} else{
// Basically, turn off specific platform fee
// Note this means that the _platformFee argument is irrelevant, the default will be used
customPlatformFeeAssigned[_merchant] = false;
customPlatformFee[_merchant] = 0;
}
emit SetMerchantSpecificPlatformFee(_merchant, _platformFee, _activated);
}
/// @dev SetMerchantSpecificExtraBudgetLockTime
/// @param _merchant The merchant that will have extra budget lock in time modified
/// @param _lockTimeSeconds Seconds to hold on to extra budget expenditure before allowing merchant to withdraw it
function setMerchantSpecificExtraBudgetLockTime(address _merchant, uint256 _lockTimeSeconds) public onlyOwner {
customLockDownPeriodExtraBudgetPaymentMerchants[_merchant] = _lockTimeSeconds;
emit SetMerchantSpecificExtraBudgetLockTime(_merchant, _lockTimeSeconds);
}
/// @dev GetMerchantSpecificExtraBudgetLockTime
/// @param _merchant The merchant to check extra budget lock time on
function getMerchantSpecificExtraBudgetLockTime(address _merchant) public view returns (uint256) {
if(customLockDownPeriodExtraBudgetPaymentMerchants[_merchant] == 0) {
return lockDownPeriodExtraBudgetPayment;
}
return customLockDownPeriodExtraBudgetPaymentMerchants[_merchant];
}
/// @dev PlatformFee
/// @param _merchant The merchant we want to get the platform fee rate for, either custom defined or default
function platformFee(address _merchant) public view returns (uint256) {
if(customPlatformFeeAssigned[_merchant]){
return customPlatformFee[_merchant];
} else {
return defaultPlatformFee;
}
}
/// @dev CreateNewOrder
/// @param _chargePerInterval Cost of the order every interval
/// @param _extraBudgetPerInterval Every interval, this amount of budget can be spent by the merchant for extra charges.
/// @param _intervalDuration The duration of the interval - seconds 9, minutes 8, hourly 7, daily 6, weekly 5, bi-weekly 4, monthly 3, quarter-year 2, bi-yearly 1, yearly 0
/// @param _erc20 Address of the payment token
/// @param _merchantDefaultNumberOfOrderIntervals Default number of intervals to approve
/// @param _trialIntervals Number of intervals that are free
function createNewOrder(uint256 _chargePerInterval, uint256 _extraBudgetPerInterval, uint256 _intervalDuration, IERC20 _erc20, uint256 _merchantDefaultNumberOfOrderIntervals, uint256 _trialIntervals) public {
createNewOrderWithPassthrough(_chargePerInterval,_extraBudgetPerInterval,_intervalDuration,_erc20,_merchantDefaultNumberOfOrderIntervals,_trialIntervals,"NONE");
}
/// @dev CreateNewOrderWithPassthrough
/// @param _chargePerInterval Cost of the order every interval
/// @param _extraBudgetPerInterval Every interval, this amount of budget can be spent by the merchant for extra charges.
/// @param _intervalDuration The duration of the interval - seconds 9, minutes 8, hourly 7, daily 6, weekly 5, bi-weekly 4, monthly 3, quarter-year 2, bi-yearly 1, yearly 0
/// @param _erc20 Address of the payment token
/// @param _merchantDefaultNumberOfOrderIntervals Default number of intervals to approve
/// @param _trialIntervals Number of intervals that are free
/// @param _passthrough The passthrough
function createNewOrderWithPassthrough(uint256 _chargePerInterval, uint256 _extraBudgetPerInterval, uint256 _intervalDuration, IERC20 _erc20, uint256 _merchantDefaultNumberOfOrderIntervals, uint256 _trialIntervals, string memory _passthrough) public {
require(_intervalDuration < 10, "Interval duration between 0 and 9");
// Supports interface
bool worked = false;
if (address(_erc20).code.length > 0) {
try _erc20.totalSupply() returns (uint v){
if(v > 0) {
Order storage order = orders[nextOrder];
order.orderId = nextOrder;
order.merchant = msg.sender;
order.chargePerInterval = _chargePerInterval;
order.extraBudgetPerInterval = _extraBudgetPerInterval;
order.startTime = _getNow();
order.intervalDuration = _intervalDuration;
order.erc20 = address(_erc20);
order.paused = false;
order.trialIntervals = _trialIntervals;
require(_merchantDefaultNumberOfOrderIntervals > 0, "Default number of intervals must be above 0");
order.merchantDefaultNumberOfOrderIntervals = _merchantDefaultNumberOfOrderIntervals;
emit OrderCreated(
nextOrder,
msg.sender,
_chargePerInterval,
_extraBudgetPerInterval,
order.startTime,
_intervalDuration,
address(_erc20),
_merchantDefaultNumberOfOrderIntervals,
_trialIntervals,
_passthrough
);
nextOrder = nextOrder + 1;
worked = true;
} else {
worked = false;
}
} catch Error(string memory revertReason) {
worked = false;
} catch (bytes memory returnData) {
worked = false;
}
}
require(worked, "ERC20 token not compatible");
}
/// @dev GetOrder
/// @param _orderId The id of the order to get information for
function getOrder(uint256 _orderId) external view returns
(uint256 orderId, address merchant, uint256 chargePerInterval, uint256 extraBudgetPerInterval, uint256 startTime, uint256 intervalDuration, address erc20, bool paused, uint256 merchantDefaultNumberOfOrderIntervals, uint256 trialIntervals){
Order storage order = orders[_orderId];
return (
order.orderId,
order.merchant,
order.chargePerInterval,
order.extraBudgetPerInterval,
order.startTime,
order.intervalDuration,
order.erc20,
order.paused,
order.merchantDefaultNumberOfOrderIntervals,
order.trialIntervals
);
}
/// @dev GetCustomerOrder
/// @param _orderId The id of the order to get information for
/// @param _customerId Bytes32 customer id for this specific subscription
function getCustomerOrder(uint256 _orderId, bytes32 _customerId) external view returns
(bytes32 customerId,
address customerAddress,
uint256 extraBudgetPerInterval,
uint256 extraBudgetUsed,
uint256 extraBudgetLifetime,
uint256 approvedPeriodsRemaining, // This number is based on the registration, it is default 36 months of reg
uint256 trialIntervalsRemaining,
uint256 firstPaymentMadeTimestamp,
uint256 numberOfIntervalsPaid,
bool terminated,
uint256 amountPaidToDate){
CustomerOrder storage order = orders[_orderId].customerOrders[_customerId];
return (
order.customerId,
order.customerAddress,
order.extraBudgetPerInterval,
order.extraBudgetUsed,
order.extraBudgetLifetime,
order.approvedPeriodsRemaining,
order.trialIntervalsRemaining,
order.firstPaymentMadeTimestamp,
order.numberOfIntervalsPaid,
order.terminated,
order.amountPaidToDate
);
}
/// @dev GetPaymentHistoryEntry
/// @param _orderId The id of the order to get information for
/// @param _customerId Customer bytes32 ID
/// @param _index A specific entry of payment history
function getPaymentHistoryEntry(uint256 _orderId, bytes32 _customerId, uint256 _index) external view returns
(uint256 timestamp, uint256 amount, uint256 feePercentage){
return (
customerHistoryTimestamps[_orderId][_customerId][_index],
customerHistoryAmounts[_orderId][_customerId][_index],
customerHistoryFeePercentages[_orderId][_customerId][_index]
);
}
/// @dev SetMerchantDefaultNumberOfOrderIntervals
/// @param _orderId The id of the order
/// @param _defaultNumberOfOrderIntervals The number of order intervals that the merchant wants to approve
function setMerchantDefaultNumberOfOrderIntervals(uint256 _orderId, uint256 _defaultNumberOfOrderIntervals) external{
Order storage order = orders[_orderId];
require(order.merchant == msg.sender || owner == msg.sender, "Only the merchant or owner can call");
order.merchantDefaultNumberOfOrderIntervals = _defaultNumberOfOrderIntervals;
emit OrderSetMerchantDefaultNumberOfOrderIntervals(_orderId, _defaultNumberOfOrderIntervals, msg.sender);
}
/// @dev SetOrderPauseState
/// @param _orderId The id of the order
/// @param _isPaused True to Pause and False to Unpause
function setOrderPauseState(uint256 _orderId, bool _isPaused) external{
Order storage order = orders[_orderId];
require(order.merchant == msg.sender || owner == msg.sender, "Only the merchant or owner can pause");
order.paused = _isPaused;
emit OrderPaused(_orderId, _isPaused, msg.sender);
}
/// @dev CustomerAcceptOrder and pay
/// @param _orderId Order id
/// @param _customerId Bytes32 Customer ID
/// @param _extraBudgetPerInterval Extra Budget that the customer is accepting per cycle
/// @param _approvedPeriods Number of periods or months accepted
function customerAcceptOrder(uint256 _orderId, bytes32 _customerId, uint256 _extraBudgetPerInterval, uint256 _approvedPeriods) public {
customerAcceptOrderWithPassthrough(_orderId, _customerId, _extraBudgetPerInterval, _approvedPeriods, "NONE");
}
/// @dev CustomerAcceptOrderWithPassthrough and pay
/// @param _orderId Order id
/// @param _customerId Bytes32 Customer ID
/// @param _extraBudgetPerInterval Extra Budget that the customer is accepting per cycle
/// @param _approvedPeriods Number of periods or months accepted
/// @param _passthrough Passthrough value
function customerAcceptOrderWithPassthrough(uint256 _orderId, bytes32 _customerId, uint256 _extraBudgetPerInterval, uint256 _approvedPeriods, string memory _passthrough) public {
Order storage order = orders[_orderId];
require(!order.paused, "Cannot process, this order is paused");
require(customerIdToAddress[_customerId] == address(0), "Can't reuse customer ids"); // TODO - possible we need to reuse customer ids but they will be unique to an order
require(order.customerOrders[_customerId].firstPaymentMadeTimestamp == 0, "This customer id is already registered on this order");
address customerAddress = msg.sender;
customerIdToAddress[_customerId] = customerAddress;
// If it is 0 use the default
if( _approvedPeriods == 0 ){
_approvedPeriods = order.merchantDefaultNumberOfOrderIntervals;
}
uint256 trialPeriodsRemaining = order.trialIntervals;
if(trialPeriodsRemaining > 0){
trialPeriodsRemaining = order.trialIntervals - 1;
customerHistoryTimestamps[_orderId][_customerId].push(_getNow());
customerHistoryAmounts[_orderId][_customerId].push(uint256(0));
customerHistoryFeePercentages[_orderId][_customerId].push(uint(0));
} else{
// Make payment if there is no free trial
uint256 calculateFee = (order.chargePerInterval * platformFee(order.merchant)) / (1000);
require(IERC20(order.erc20).allowance(msg.sender, address(this)) >= order.chargePerInterval, "Insufficient erc20 allowance");
require(IERC20(order.erc20).balanceOf(msg.sender) >= order.chargePerInterval, "Insufficient balance first month");
(bool successFee) = IERC20(order.erc20).transferFrom(msg.sender, owner, calculateFee);
require(successFee, "Fee transfer has failed");
(bool successMerchant) = IERC20(order.erc20).transferFrom(msg.sender, order.merchant, (order.chargePerInterval - calculateFee));
require(successMerchant, "Merchant transfer has failed");
customerHistoryTimestamps[_orderId][_customerId].push(_getNow());
customerHistoryAmounts[_orderId][_customerId].push( order.chargePerInterval);
customerHistoryFeePercentages[_orderId][_customerId].push(platformFee(order.merchant));
}
// Update customer histories
order.customerOrders[_customerId] = CustomerOrder({
customerId: _customerId,
customerAddress: msg.sender,
extraBudgetPerInterval: _extraBudgetPerInterval,
extraBudgetUsed: 0,
extraBudgetLifetime: 0,
approvedPeriodsRemaining: _approvedPeriods,
trialIntervalsRemaining: trialPeriodsRemaining,
terminated: false,
amountPaidToDate: order.chargePerInterval,
firstPaymentMadeTimestamp: _getNow(),
numberOfIntervalsPaid: 1
});
emit OrderAccepted(
_orderId,
_customerId,
msg.sender,
_getNow(),
_extraBudgetPerInterval,
_approvedPeriods,
trialPeriodsRemaining,
_passthrough);
}
/// @dev BatchProcessPayment
/// @param _orderIds Order ids
/// @param _customerIds The customers bytes32 id array, it must be the same length as the order id array
/// @param _gasSavingMode False will trigger erc20 tokens to go directly to the merchant. True will use gas saving mode to escrow payments for later withdrawal by the merchant
/// @param _extraAmounts If there will be extra amounts on a subscription charged, the amount that corresponds to previous arrays
function batchProcessPayment(uint256[] memory _orderIds, bytes32[] memory _customerIds, bool _gasSavingMode, uint256[] memory _extraAmounts) external {
string[] memory _passthrough = new string[](_orderIds.length);
for (uint256 i = 0; i < _orderIds.length; i++) {
_passthrough[i] = "NONE";
}
batchProcessPaymentWithData(_orderIds,_customerIds,_gasSavingMode,_extraAmounts,_passthrough);
}
/// @dev BatchProcessPayment
/// @param _orderIds Order ids
/// @param _customerIds The customers bytes32 id array, it must be the same length as the order id array
/// @param _gasSavingMode False will trigger erc20 tokens to go directly to the merchant. True will use gas saving mode to escrow payments for later withdrawal by the merchant
/// @param _extraAmounts If there will be extra amounts on a subscription charged, the amount that corresponds to previous arrays
/// @param _passthrough String for merchants to include more pass through context about payment
function batchProcessPaymentWithData(uint256[] memory _orderIds, bytes32[] memory _customerIds, bool _gasSavingMode, uint256[] memory _extraAmounts, string[] memory _passthrough) public {
// Instantiate passthrough
require(_orderIds.length == _customerIds.length, "The orders and customers must be equal length");
require(_orderIds.length == _extraAmounts.length, "The orders and extra amounts must be equal length");
for(uint256 i=0; i< _orderIds.length; i++){
bool success;
string memory revertReason;
bytes memory revertData;
ProcessPaymentArgs memory args = ProcessPaymentArgs({
orderId: _orderIds[i],
customerId: _customerIds[i],
gasSavingMode: _gasSavingMode,
extraBudgetAmount: _extraAmounts[i],
passthrough: _passthrough[i]
});
(success, revertReason, revertData) = _processPayment(args);
if(success)
{
emit SuccessfulPay(_orderIds[i], _customerIds[i], customerIdToAddress[_customerIds[i]], _passthrough[i]);
} else {
if(bytes(revertReason).length > 0){
emit PaymentFailure(revertReason, _orderIds[i], _customerIds[i], customerIdToAddress[_customerIds[i]], _passthrough[i]);
} else {
emit PaymentFailureBytes(revertData, _orderIds[i], _customerIds[i], customerIdToAddress[_customerIds[i]], _passthrough[i]);
}
}
}
}
//uint256 _orderId, bytes32 _customerId, bool _gasSavingMode, uint256 _extraBudgetAmount, string memory _passthrough
function _processPayment(ProcessPaymentArgs memory args) internal returns (bool success, string memory revertCause, bytes memory revertData) {
Order storage order = orders[args.orderId];
string memory passthrough = args.passthrough;
//Need to only allow owner, operator, or merchant to process payment
require(msg.sender == operator || msg.sender == owner || msg.sender == order.merchant || msg.sender == merchantOperators[order.merchant], "Only operator, owner, merchant, or merchant operator can process payments");
require(order.customerOrders[args.customerId].firstPaymentMadeTimestamp > 0); // Need to be greater than 0 firstpayment timestamp
uint256 howManyIntervalsToPay = _howManyIntervalsToPay(order, args.customerId);
//
// if (elapsedIntervals > customerOrder.numberOfIntervalsPaid) {
// customerOrder.extraBudgetUsed = 0; // Reset the extra budget when entering a new interval
// }
if(howManyIntervalsToPay > order.customerOrders[args.customerId].approvedPeriodsRemaining){
howManyIntervalsToPay = order.customerOrders[args.customerId].approvedPeriodsRemaining;
}
uint256 howManyIntervalsMinusTrialIntervals = 0;
if(howManyIntervalsToPay > order.customerOrders[args.customerId].trialIntervalsRemaining){
howManyIntervalsMinusTrialIntervals = howManyIntervalsToPay - order.customerOrders[args.customerId].trialIntervalsRemaining;
} else {
howManyIntervalsMinusTrialIntervals = 0;
}
bool terminated = order.customerOrders[args.customerId].terminated;
uint256 howMuchERC20ToSend = howManyIntervalsMinusTrialIntervals * order.chargePerInterval;
uint256 calculateFee = (howMuchERC20ToSend * platformFee(order.merchant)) / (1000);
if(!args.gasSavingMode){
try SubscriptionApp(this).payOutMerchantAndFeesInternalMethod(args.customerId, howMuchERC20ToSend, calculateFee, order.paused, terminated, order.merchant, order.erc20
) {
order.customerOrders[args.customerId].numberOfIntervalsPaid = order.customerOrders[args.customerId].numberOfIntervalsPaid + howManyIntervalsToPay;
order.customerOrders[args.customerId].approvedPeriodsRemaining = order.customerOrders[args.customerId].approvedPeriodsRemaining - howManyIntervalsToPay;
if(order.customerOrders[args.customerId].trialIntervalsRemaining >= howManyIntervalsToPay){
order.customerOrders[args.customerId].trialIntervalsRemaining = order.customerOrders[args.customerId].trialIntervalsRemaining - howManyIntervalsToPay;
} else {
order.customerOrders[args.customerId].trialIntervalsRemaining = 0;
}
if(howMuchERC20ToSend > 0) {
order.customerOrders[args.customerId].amountPaidToDate = order.customerOrders[args.customerId].amountPaidToDate + howMuchERC20ToSend;
// Update customer histories
customerHistoryTimestamps[args.orderId][args.customerId].push(_getNow());
customerHistoryAmounts[args.orderId][args.customerId].push( order.chargePerInterval);
customerHistoryFeePercentages[args.orderId][args.customerId].push(platformFee(order.merchant));
emit OrderPaidOut(
args.orderId,
args.customerId,
howMuchERC20ToSend,
calculateFee,
_getNow(),
tx.origin,
passthrough
);
}
// First process extra budget payment
if(args.extraBudgetAmount > 0) {
ExtraBudgetArgs memory extraArgs = ExtraBudgetArgs({
orderId: args.orderId,
customerId: args.customerId,
extraAmount: args.extraBudgetAmount,
pendingIntervals: howManyIntervalsToPay,
passthrough: args.passthrough
});
(bool success, string memory revertReason) = processExtraBudgetPayment(extraArgs);
if (!success) {
return (false, revertReason, "");
}
}
return (true, "", "");
} catch Error(string memory revertReason) {
return (false, revertReason, "");
} catch (bytes memory returnData) {
return (false, "", returnData);
}
} else{
// Gas saving mode holds on to balances accounting for the merchants and owner
try SubscriptionApp(this).payOutGasSavingInternalMethod(args.customerId, howMuchERC20ToSend, order.paused, terminated, order.erc20
) {
order.customerOrders[args.customerId].numberOfIntervalsPaid = order.customerOrders[args.customerId].numberOfIntervalsPaid + howManyIntervalsToPay;
order.customerOrders[args.customerId].approvedPeriodsRemaining = order.customerOrders[args.customerId].approvedPeriodsRemaining - howManyIntervalsToPay;
if(order.customerOrders[args.customerId].trialIntervalsRemaining >= howManyIntervalsToPay){
order.customerOrders[args.customerId].trialIntervalsRemaining = order.customerOrders[args.customerId].trialIntervalsRemaining - howManyIntervalsToPay;
} else {
order.customerOrders[args.customerId].trialIntervalsRemaining = 0;
}
if(howMuchERC20ToSend > 0) {
order.customerOrders[args.customerId].amountPaidToDate = order.customerOrders[args.customerId].amountPaidToDate + howMuchERC20ToSend;
// Update customer histories
customerHistoryTimestamps[args.orderId][args.customerId].push(_getNow());
customerHistoryAmounts[args.orderId][args.customerId].push( order.chargePerInterval);
customerHistoryFeePercentages[args.orderId][args.customerId].push(platformFee(order.merchant));
// Update balance -- this is the different part of code
pendingOwnerWithdrawalAmountByToken[order.erc20] += calculateFee;
pendingMerchantWithdrawalAmountByMerchantAndToken[order.merchant][order.erc20] += (howMuchERC20ToSend - calculateFee);
emit OrderPaidOutGasSavingMode(
args.orderId,
args.customerId,
howMuchERC20ToSend,
calculateFee,
_getNow(),
tx.origin,
passthrough
);
}
// First process extra budget payment
if (args.extraBudgetAmount > 0) {
ExtraBudgetArgs memory extraArgs = ExtraBudgetArgs({
orderId: args.orderId,
customerId: args.customerId,
extraAmount: args.extraBudgetAmount,
pendingIntervals: howManyIntervalsToPay,
passthrough: args.passthrough
});
(bool success, string memory revertReason) = processExtraBudgetPayment(extraArgs);
if (!success) {
return (false, revertReason, "");
}
}
return (true, "", "");
} catch Error(string memory revertReason) {
return (false, revertReason, "");
} catch (bytes memory returnData) {
return (false, "", returnData);
}
}
}
function payOutMerchantAndFeesInternalMethod(
bytes32 _customerId,
uint256 howMuchERC20ToSend,
uint256 calculateFee,
bool orderPaused,
bool terminated,
address orderMerchant,
address orderErc20) external {
require(msg.sender == address(this), "Internal calls only");
require(!terminated, "This payment has been cancelled");
require(!orderPaused, "Cannot process, this order is paused");
require(IERC20(orderErc20).allowance(customerIdToAddress[_customerId], address(this)) >= howMuchERC20ToSend, "Insufficient erc20 allowance");
require(IERC20(orderErc20).balanceOf(customerIdToAddress[_customerId]) >= howMuchERC20ToSend, "Insufficient balance");
(bool successFee) = IERC20(orderErc20).transferFrom(customerIdToAddress[_customerId], owner, calculateFee);
require(successFee, "Fee transfer has failed");
(bool successMerchant) = IERC20(orderErc20).transferFrom(customerIdToAddress[_customerId], orderMerchant, (howMuchERC20ToSend - calculateFee));
require(successMerchant, "Merchant transfer has failed");
}
function payOutGasSavingInternalMethod(
bytes32 _customerId,
uint256 howMuchERC20ToSend,
bool orderPaused,
bool terminated,
address orderErc20) external {
require(msg.sender == address(this), "Internal calls only");
require(!terminated, "This payment has been cancelled");
require(!orderPaused, "Cannot process, this order is paused");
require(IERC20(orderErc20).allowance(customerIdToAddress[_customerId], address(this)) >= howMuchERC20ToSend, "Insufficient erc20 allowance");
require(IERC20(orderErc20).balanceOf(customerIdToAddress[_customerId]) >= howMuchERC20ToSend, "Insufficient balance");
(bool successPayment) = IERC20(orderErc20).transferFrom(customerIdToAddress[_customerId], address(this), howMuchERC20ToSend);
require(successPayment, 'Token transfer unsuccessful');
}
// Check how much erc20 amount is ready for payment
function howManyIntervalsToPayExternal(uint256 _orderId, bytes32 _customerId) external view returns (uint256 howManyPayableIntervals){
Order storage order = orders[_orderId];
return _howManyIntervalsToPay(order, _customerId);
}
// Check how much erc20 amount is ready for payment
function _howManyIntervalsToPay(Order storage order, bytes32 _customerId) internal view returns (uint256){
// Pick the mode of the invoicing
uint256 customerCycleStartTime = order.customerOrders[_customerId].firstPaymentMadeTimestamp;
uint256 numberOfIntervalsPaid = order.customerOrders[_customerId].numberOfIntervalsPaid;
require(order.intervalDuration < 10, "The cycle mode is not correctly configured");
uint256 elapsedCycles = 0;
// Use cycle mode in switch statement
// We find number of cycles that have elapsed since the first payment was made, and deduce from there with how many have been numberOfIntervalsPaid
if(order.intervalDuration == 0){
// Yearly Payment
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffYears(customerCycleStartTime, _getNow()));
} else if(order.intervalDuration == 1){
// 6 Month Payment
elapsedCycles = (elapsedCycles + (BokkyPooBahsDateTimeLibrary.diffMonths(customerCycleStartTime, _getNow()))/ 6);
} else if(order.intervalDuration == 2){
// 3 Month payment
elapsedCycles = (elapsedCycles + (BokkyPooBahsDateTimeLibrary.diffMonths(customerCycleStartTime, _getNow()))/ 3);
} else if(order.intervalDuration == 3){
// Monthly payment
// Logic for these is that we add the number of passed months
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffMonths(customerCycleStartTime, _getNow()));
} else if (order.intervalDuration == 4){
// Bi-weekly payment
elapsedCycles = (elapsedCycles + (BokkyPooBahsDateTimeLibrary.diffDays(customerCycleStartTime, _getNow()) / 14));
} else if (order.intervalDuration == 5){
// Weekly payment
elapsedCycles = (elapsedCycles + (BokkyPooBahsDateTimeLibrary.diffDays(customerCycleStartTime, _getNow()) / 7));
} else if (order.intervalDuration == 6){
// Daily payment
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffDays(customerCycleStartTime, _getNow()));
} else if (order.intervalDuration == 7){
// Hourly payment
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffHours(customerCycleStartTime, _getNow()));
} else if (order.intervalDuration == 8){
// Minute payment
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffMinutes(customerCycleStartTime, _getNow()));
} else {
// Second payment
elapsedCycles = (elapsedCycles + BokkyPooBahsDateTimeLibrary.diffSeconds(customerCycleStartTime, _getNow()));
}
// Return the number of chargeable cycles
return elapsedCycles - (numberOfIntervalsPaid - 1);
}
function processExtraBudgetPayment(
ExtraBudgetArgs memory args
) internal returns (bool success, string memory revertReason) {
Order storage order = orders[args.orderId];
CustomerOrder storage customerOrder = order.customerOrders[args.customerId];
uint256 availableBudget = ((1 + args.pendingIntervals) * customerOrder.extraBudgetPerInterval)
- customerOrder.extraBudgetUsed;
if (args.extraAmount > availableBudget) {
return (false, "Exceeds extra budget");
}
if ((availableBudget - args.extraAmount) > customerOrder.extraBudgetPerInterval) {
// Still enough left for the next period, reset used
customerOrder.extraBudgetUsed = 0;
} else {
// Track how much budget has been used this interval
customerOrder.extraBudgetUsed = customerOrder.extraBudgetPerInterval - (availableBudget - args.extraAmount);
}
customerOrder.extraBudgetLifetime += args.extraAmount;
PendingExtraBudgetPayment memory newPayment = PendingExtraBudgetPayment({
customerId: args.customerId,
customerAddress: customerIdToAddress[args.customerId],
timestamp: _getNow(),
extraAmount: args.extraAmount,
processed: false,
refunded: false
});
pendingExtraBudgetPaymentListByOrderAndCustomer[args.orderId].push(newPayment);
uint256 len = pendingExtraBudgetPaymentListByOrderAndCustomer[args.orderId].length;
bool successExtraPayment = IERC20(order.erc20).transferFrom(
customerIdToAddress[args.customerId],
address(this),
args.extraAmount
);
if (!successExtraPayment) {
return (false, "Fee transfer has failed");
}
emit ExtraBudgetLogged(
args.orderId,
args.customerId,
customerIdToAddress[args.customerId],
args.extraAmount,
args.pendingIntervals,
len - 1,
args.passthrough
);
return (true, "");
}
// Function to process pending extra budget payments for a specific order, with an option to limit the range of payments processed
// Use 0 as endPaymentIndex to process everything after startPaymentIndex
/// @dev processPendingPayments
/// @param orderId Order id of the order to process extra pending payments for
/// @param startPaymentIndex Index to start processing payments at
/// @param endPaymentIndex Index to end processing payments at, or 0 to process all
function processPendingPayments(uint256 orderId, uint256 startPaymentIndex, uint256 endPaymentIndex) external {
Order storage order = orders[orderId];
require(orderId < nextOrder, "Invalid order ID");
require(endPaymentIndex == 0 || endPaymentIndex > startPaymentIndex, "End index must be zero or greater than start index");
uint256 currentTime = _getNow();
uint256 totalAmountToTransfer = uint256(0);
uint256 paymentsCount = pendingExtraBudgetPaymentListByOrderAndCustomer[orderId].length;
uint256 upperBound = (endPaymentIndex == 0 || endPaymentIndex > paymentsCount) ? paymentsCount : endPaymentIndex + 1;
for (uint256 i = startPaymentIndex; i < upperBound; i++) {
PendingExtraBudgetPayment storage payment = pendingExtraBudgetPaymentListByOrderAndCustomer[orderId][i];
if (!payment.processed && ((currentTime - payment.timestamp) >= getMerchantSpecificExtraBudgetLockTime(order.merchant))) {
totalAmountToTransfer += payment.extraAmount;
payment.processed = true;
emit ExtraBudgetPaymentProcessed(orderId, payment.customerId, payment.customerAddress, payment.extraAmount, i);
}
}
if (totalAmountToTransfer > 0) {
address erc20Token = orders[orderId].erc20;
address merchant = orders[orderId].merchant;
// require(IERC20(erc20Token).transfer(merchant, totalAmountToTransfer), "Transfer failed");
uint256 calculateFee = (totalAmountToTransfer * platformFee(merchant)) / (1000);
(bool successFee) = IERC20(erc20Token).transfer(owner, calculateFee);
require(successFee, "Fee transfer has failed");
(bool successMerchant) = IERC20(erc20Token).transfer(merchant, (totalAmountToTransfer - calculateFee));
require(successMerchant, "Merchant transfer has failed");
emit ExtraBudgetPaidOut(orderId, totalAmountToTransfer, startPaymentIndex, upperBound);
}
}
// Function to refund pending extra budget payments for a specific order, with an option to limit the range of payments processed
// Use 0 as endPaymentIndex to process everything after startPaymentIndex
/// @dev refundPendingExtraPayment
/// @param orderId Order id of the order to refund extra pending payments for
/// @param startRefundIndex Index to start refund payments at
/// @param endRefundIndex Index to end refund payments at, or 0 to process all
function refundPendingExtraPayment(uint256 orderId, uint256 startRefundIndex, uint256 endRefundIndex) external onlyOwner {
Order storage order = orders[orderId];
require(msg.sender == order.merchant || msg.sender == owner, "Only the merchant or the contract owner can issue refunds");
require(orderId < nextOrder, "Invalid order ID");
require(endRefundIndex == 0 || endRefundIndex > startRefundIndex, "End index must be zero or greater than start index");
uint256 paymentsCount = pendingExtraBudgetPaymentListByOrderAndCustomer[orderId].length;
uint256 upperBound = (endRefundIndex == 0 || endRefundIndex > paymentsCount) ? paymentsCount : endRefundIndex + 1;
address erc20Token = orders[orderId].erc20;
for (uint256 i = startRefundIndex; i < upperBound; i++) {
PendingExtraBudgetPayment storage payment = pendingExtraBudgetPaymentListByOrderAndCustomer[orderId][i];
if (!payment.processed) { // Payment must not yet be processed, so has not been paid or refunded
payment.processed = true;
payment.refunded = true;
require(IERC20(erc20Token).transfer(payment.customerAddress, payment.extraAmount), "Refund failed"); // Extra amount sent BACK to the customer
emit ExtraBudgetRefunded(orderId, payment.customerId, payment.customerAddress, payment.extraAmount, i);
}
}
}
/// @dev CustomerRenewOrder
/// @param _orderId Order Id
/// @param _customerId Customer id
/// @param _extraBudgetPerInterval Extra budget allowed per interval
/// @param _approvedPeriods If renewing , it sets this amount, if extending it adds this amount
function customerRenewOrder(uint256 _orderId, bytes32 _customerId, uint256 _extraBudgetPerInterval, uint256 _approvedPeriods) external {
customerRenewOrderWithPassthrough(_orderId, _customerId, _extraBudgetPerInterval, _approvedPeriods, "NONE");
}
/// @dev CustomerRenewOrder
/// @param _orderId Order Id
/// @param _customerId Customer id
/// @param _extraBudgetPerInterval Extra budget allowed per interval
/// @param _approvedPeriods If renewing , it sets this amount, if extending it adds this amount
/// @param _passthrough Passthrough Information
function customerRenewOrderWithPassthrough(uint256 _orderId, bytes32 _customerId, uint256 _extraBudgetPerInterval, uint256 _approvedPeriods, string memory _passthrough) public {
Order storage order = orders[_orderId];
require(msg.sender == customerIdToAddress[_customerId], "Can only renew your own orders");
CustomerOrder storage customerOrder = orders[_orderId].customerOrders[_customerId];
require(customerOrder.firstPaymentMadeTimestamp > 0, "Not valid customer to renew");
if( _approvedPeriods == 0 ){
_approvedPeriods = order.merchantDefaultNumberOfOrderIntervals;
}
if(customerOrder.terminated){
// The order was previously cancelled
// Pays for first month
require(IERC20(order.erc20).allowance(msg.sender, address(this)) >= order.chargePerInterval, "Insufficient erc20 allowance");
require(IERC20(order.erc20).balanceOf(msg.sender) >= order.chargePerInterval, "Insufficient balance first month");
uint256 calculateFee = (order.chargePerInterval * platformFee(order.merchant)) / (1000);
(bool successFee) = IERC20(order.erc20).transferFrom(msg.sender, owner, calculateFee);
require(successFee, 'Fee transfer erc20 failed');
(bool successMerchant) = IERC20(order.erc20).transferFrom(msg.sender, order.merchant, (order.chargePerInterval - calculateFee));
require(successMerchant, 'Merchant transfer erc20 failed');
// Update customer histories
customerHistoryTimestamps[_orderId][_customerId].push(_getNow());
customerHistoryAmounts[_orderId][_customerId].push( order.chargePerInterval);
customerHistoryFeePercentages[_orderId][_customerId].push(platformFee(order.merchant));
customerOrder.approvedPeriodsRemaining = _approvedPeriods;
customerOrder.numberOfIntervalsPaid = 1;
customerOrder.firstPaymentMadeTimestamp = _getNow();
customerOrder.amountPaidToDate = customerOrder.amountPaidToDate + order.chargePerInterval;
customerOrder.extraBudgetUsed = 0;
customerOrder.extraBudgetPerInterval = _extraBudgetPerInterval;
customerOrder.terminated = false;
emit OrderRenewed(
_orderId,
_customerId,
msg.sender,
_getNow(),
_approvedPeriods,
true,
_passthrough);
} else {
customerOrder.approvedPeriodsRemaining = customerOrder.approvedPeriodsRemaining + _approvedPeriods;
customerOrder.extraBudgetUsed = 0;
customerOrder.extraBudgetPerInterval = _extraBudgetPerInterval;
emit OrderRenewed(
_orderId,
_customerId,
msg.sender,
customerOrder.firstPaymentMadeTimestamp,
_approvedPeriods,
false,
_passthrough);
}
}
/// @dev CustomerCancelPayment
/// @param _orderId Order id
/// @param _customerId Bytes32 Customer ID
function customerCancelOrder(uint256 _orderId, bytes32 _customerId) external {
Order storage order = orders[_orderId];
require((customerIdToAddress[_customerId] == msg.sender) || (owner == msg.sender)
|| (order.merchant == msg.sender), "Only the customer, merchant, or owner can cancel an order");
order.customerOrders[_customerId].terminated = true;
order.customerOrders[_customerId].approvedPeriodsRemaining = 0;
order.customerOrders[_customerId].trialIntervalsRemaining = 0;
order.customerOrders[_customerId].extraBudgetPerInterval = 0;
emit OrderCancelled(
_orderId,
_customerId,
customerIdToAddress[_customerId]);
}
/// @dev Withdraw
/// @param _erc20Token ERC20 to withdraw for msg sender merchant
function withdraw(address _erc20Token) public {
uint256 value = 0;
if(msg.sender == owner){
value = pendingOwnerWithdrawalAmountByToken[_erc20Token];
pendingOwnerWithdrawalAmountByToken[_erc20Token] = 0;
(bool successWithdraw) = IERC20(_erc20Token).transfer(
owner,
value);
require(successWithdraw, 'ERC20 Withdrawal was unsuccessful');
emit OwnerWithdrawERC20(_erc20Token, value);
} else {
value = pendingMerchantWithdrawalAmountByMerchantAndToken[msg.sender][_erc20Token];
pendingMerchantWithdrawalAmountByMerchantAndToken[msg.sender][_erc20Token] = 0;
(bool successWithdraw) = IERC20(_erc20Token).transfer(
msg.sender,
value);
require(successWithdraw, 'ERC20 Withdrawal was unsuccessful');
emit MerchantWithdrawERC20(_erc20Token, msg.sender, value);
}
}
/// @dev Withdraw
/// @param _erc20Tokens ERC20 list to withdraw for msg sender merchant
function withdrawBatch(address[] memory _erc20Tokens) external {
for(uint256 i=0; i< _erc20Tokens.length; i++){
withdraw(_erc20Tokens[i]);
}
}
/// @dev Withdraw
/// @param _user Which merchant to withdraw for, any account can spend gas to withdraw these funds for another
/// @param _erc20Token ERC20 to withdraw
function withdrawForUser(address _user, address _erc20Token) public { // For merchant
uint256 value = pendingMerchantWithdrawalAmountByMerchantAndToken[_user][_erc20Token];
IERC20(_erc20Token).transfer(
_user,
value);
pendingMerchantWithdrawalAmountByMerchantAndToken[_user][_erc20Token] = 0;
emit MerchantWithdrawERC20(_erc20Token, _user, value);
}
/// @dev OwnerEmergencyRecover
/// @param _amount Amount of erc20
/// @param _erc20Token ERC20 to withdraw by Owner in emergencies
function ownerEmergencyRecover(uint256 _amount, address _erc20Token) public onlyOwner{
IERC20(_erc20Token).transfer(
owner,
_amount);
}
/// @dev AddYearsToTimestamp
/// @param _timestamp Timestamp
/// @param _years Years to add to timestamp
function addYearsToTimestamp(uint _timestamp, uint _years) external view returns (uint newTimestamp){
return BokkyPooBahsDateTimeLibrary.addYears(_timestamp, _years);
}
/// @dev AddMonthsToTimestamp
/// @param _timestamp Timestamp
/// @param _months Months to add to timestamp
function addMonthsToTimestamp(uint _timestamp, uint _months) external view returns (uint newTimestamp){
return BokkyPooBahsDateTimeLibrary.addMonths(_timestamp, _months);
}
function _getNow() internal virtual view returns (uint256) {
return block.timestamp;
}
}
Submitted on: 2025-11-07 12:08:02
Comments
Log in to comment.
No comments yet.