Thanks to the post on Creating customer payment method with api I am able to create a new Payment Method successfully using the API. But I can't figure out how to update or delete an existing payment method. There doesn't seem to be any type of key field. I've updated and deleted Contacts and Locations with the API, but Contacts have Contact IDs, and Locations have LocationIDs. There doesn't seem to be a PaymentMethodID. I thought maybe the "Card/Account No." field might function as that, but I wasn't able to make that work. And I've noticed that I can save two Payment Methods with identical card numbers, so that would make identical keys if that field was used anyway.
Here's the code I tried for updating. It's mostly Chris's code from the other post I linked to, but trying to add an extra key field for the update.
Public Function UpdateCreditCard(ByVal customerID As String, ByVal existingAcctNum As String, ByVal paymentMethodCode As String, ByVal CCNum As String, ByVal expiration As String, ByVal nameOnCard As String, ByVal active As Boolean)
Dim paymentMethod As AR303010Content = m_context.AR303010GetSchema()
m_context.AR303010Clear()
' main level fields
Dim customerVal As Value = CreateValue(paymentMethod.PaymentMethodSelection.Customer, customerID)
Dim paymentMethodCodeVal As Value = CreateValue(paymentMethod.PaymentMethodSelection.PaymentMethod, paymentMethodCode)
' inner level fields
Dim ccNumName As Value = CreateValue(paymentMethod.PaymentMethodDetails.Description, "CCDNUM")
Dim ccNumValue As Value = CreateValue(paymentMethod.PaymentMethodDetails.Value, CCNum, True)
Dim ccExpName As Value = CreateValue(paymentMethod.PaymentMethodDetails.Description, "EXPDATE")
Dim ccExpValue As Value = CreateValue(paymentMethod.PaymentMethodDetails.Value, expiration, True)
Dim ccNameName As Value = CreateValue(paymentMethod.PaymentMethodDetails.Description, "NAMEONCC")
Dim ccNameValue As Value = CreateValue(paymentMethod.PaymentMethodDetails.Value, nameOnCard, True)
Dim saveCommands() As Command
If existingAcctNum = "" Then
' new credit card
saveCommands = {customerVal, paymentMethod.Actions.Insert, paymentMethodCodeVal, ccNumName, ccNumValue, ccExpName, ccExpValue, ccNameName, ccNameValue, paymentMethod.Actions.Save}
Else
' existing credit card, only allow update of Active or expiration based on "description"
Dim descriptionVal As Value = CreateValue(paymentMethod.PaymentMethodDetails.Description, existingAcctNum)
Dim activeVal As Value = CreateValue(paymentMethod.PaymentMethodSelection.Active, active.ToString())
saveCommands = {customerVal, descriptionVal, ccExpName, ccExpValue, activeVal, paymentMethod.Actions.Save}
End If
Dim updateResult As AR303010Content() = m_context.AR303010Submit(saveCommands)
Return ""
End Function
Another confusing part of this is that the two fields I would really like to let the user update are either the expiration date, or whether the card is Active. But the Active flag is one of the fields in the list accessible from the Customer screen, and the expiration date is a field only accessible from the Payment Method screen. If those are two different API calls, then will I need a different key field in each one of them?
This screen is difficult to use via web services for a few different reasons:
There is no unique key exposed via the screen that you can use to locate payment methods. The internal key is a field named PMInstanceID, and although you can technically refer to it via web service commands, it is not obvious to use
In Acumatica, payment method details are locked once one transaction using this payment method has been recorded. This means that to make modifications to it, you need to create a new payment method, and set the old one inactive.
There's a bug in the screen that only manifests when using it via web services, which is related to item above. When loading a payment method, system will disable the payment method details if there has been any transactions, but will fail to re-enable it if not. This is never a problem when using Acumatica from a web browser, since the fields are re-enabled automatically between every round-trip. But when automating this screen through web services, you're actually performing all the operations in a single round-trip. This problem has been reported and will be fixed shortly (internal JIRA reference is AC-54456)
That being said, there's a few things at our disposal that we can use to make this screen as easy to use from web services as other screens, and work around its limitations. By creating a customization project, we can add the PMInstanceID field (hereby referred to as the Token ID field) to the screen. You can store this token on your system, and use it for future operations with the payment method. Within the same customization project, we can also make the payment method details always enabled, allowing you to update expiration date on existing cards. Doing so also resolves the bug mentioned above where system won't allow you to add any new payment method to the system. The customization has two parts:
Overriding CustomerPaymentMethod DAC to make the PMInstanceID field accessible from the UI. This is done by appending the PXUIField attribute to the PMInstanceID field: [PXUIField(DisplayName="Token ID", Visibility=PXUIVisibility.SelectorVisible)]. After that, the field may be added to the screen using the layout editor.
Handling the CustomerPaymentMethod_RowSelected event to force the payment methods details to always be enabled. We also use this event handler to hide the Token ID field when adding a new payment method, since this field would otherwise show int.MinValue until the payment method has been saved. The full code of the event looks like this:
protected void CustomerPaymentMethod_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
// Force the payment method details to always be enabled to facilitate working via web services
PXUIFieldAttribute.SetEnabled(Base.Details.Cache, null, true);
// When adding a new method, field will have a temporary value corresponding to int.MinValue - don't show it
PXUIFieldAttribute.SetVisible<CustomerPaymentMethod.pMInstanceID>(cache, e.Row, cache.GetStatus(e.Row) != PXEntryStatus.Inserted);
}
Once the customization is published, it becomes far easier to update existing payment methods. The code below shows how you can add a payment method, and retrieve the token ID that you will use later on to update the payment method:
public int AddCreditCard(string customerID, string paymentMethod, string cardNumber, string expirationDate, string cvv, string nameOnCard)
{
if(_AR303010 == null) _AR303010 = _context.AR303010GetSchema();
_context.AR303010Clear();
var commands = new Command[]
{
new Value { Value = customerID, LinkedCommand = _AR303010.PaymentMethodSelection.Customer },
new Value { Commit = true, LinkedCommand = _AR303010.Actions.Insert },
new Value { Value = paymentMethod, LinkedCommand = _AR303010.PaymentMethodSelection.PaymentMethod },
new Value { Value = "CCDNUM", LinkedCommand = _AR303010.PaymentMethodDetails.Description },
new Value { Value = cardNumber, LinkedCommand = _AR303010.PaymentMethodDetails.Value, Commit = true },
new Value { Value = "EXPDATE", LinkedCommand = _AR303010.PaymentMethodDetails.Description },
new Value { Value = expirationDate, LinkedCommand = _AR303010.PaymentMethodDetails.Value, Commit = true},
new Value { Value = "CVV", LinkedCommand = _AR303010.PaymentMethodDetails.Description },
new Value { Value = cvv, LinkedCommand = _AR303010.PaymentMethodDetails.Value, Commit = true },
new Value { Value = "NAMEONCC", LinkedCommand = _AR303010.PaymentMethodDetails.Description },
new Value { Value = nameOnCard, LinkedCommand = _AR303010.PaymentMethodDetails.Value, Commit = true },
_AR303010.Actions.Save,
_AR303010.PaymentMethodSelection.TokenID
};
var result = _context.AR303010Submit(commands.ToArray());
return int.Parse(result[0].PaymentMethodSelection.TokenID.Value);
}
public void UpdateCreditCardExpirationDate(string customerID, string paymentMethod, int tokenID, string expirationDate)
{
if (_AR303010 == null) _AR303010 = _context.AR303010GetSchema();
_context.AR303010Clear();
var commands = new Command[]
{
new Value { Value = customerID, LinkedCommand = _AR303010.PaymentMethodSelection.Customer },
new Value { Commit = true, LinkedCommand = _AR303010.Actions.Insert },
new Value { Value = paymentMethod, LinkedCommand = _AR303010.PaymentMethodSelection.PaymentMethod },
new Value { Value = tokenID.ToString(), LinkedCommand = _AR303010.PaymentMethodSelection.TokenID },
new Value { Value = "EXPDATE", LinkedCommand = _AR303010.PaymentMethodDetails.Description },
new Value { Value = expirationDate, LinkedCommand = _AR303010.PaymentMethodDetails.Value, Commit = true},
_AR303010.Actions.Save,
};
var result = _context.AR303010Submit(commands.ToArray());
}
public void MakeCardInactive(string customerID, string paymentMethod, int tokenID)
{
if (_AR303010 == null) _AR303010 = _context.AR303010GetSchema();
_context.AR303010Clear();
var commands = new Command[]
{
new Value { Value = customerID, LinkedCommand = _AR303010.PaymentMethodSelection.Customer },
new Value { Commit = true, LinkedCommand = _AR303010.Actions.Insert },
new Value { Value = paymentMethod, LinkedCommand = _AR303010.PaymentMethodSelection.PaymentMethod },
new Value { Value = tokenID.ToString(), LinkedCommand = _AR303010.PaymentMethodSelection.TokenID },
new Value { Value = "False", LinkedCommand = _AR303010.PaymentMethodSelection.Active },
_AR303010.Actions.Save,
};
var result = _context.AR303010Submit(commands.ToArray());
}
I wrapped these 3 functions into a class, and actual usage becomes quite simple:
var paymentMethodManager = new PaymentMethodManager(context);
int tokenID = paymentMethodManager.AddCreditCard("ABARTENDE", "MASTERCARD", "5111111111111118", "122016", "123", "John Doe");
paymentMethodManager.UpdateCreditCardExpirationDate("ABARTENDE", "MASTERCARD", tokenID, "032017");
paymentMethodManager.MakeCardInactive("ABARTENDE", "MASTERCARD", tokenID);
As of now, it is not possible to delete an existing payment method, and you must make it inactive instead. I've made the request for this enhancement, and it may come in the future.
Note: I have placed all the code used in this answer on GitHub at https://github.com/gmichaud/acumatica-paymentmethod-ws-extensions.
Related
I need to store user's info in DynamoDB and send a mail to the same user if it doesn't already exist in DynamoDB table. I am doing this in for loop. The list contains only 2 records. The issue is only the second record gets inserted in table and the mail is sent twice to the same user. Here is the code:
module.exports.AddUser = function(req, res, usersList, departmentId) {
var _emailId = "";
var _userName = "";
var _departmentId = departmentId;
for (var i = 0; i < usersList.length; i++) {
_emailId = usersList[i].emailId;
_userName = usersList[i].userName;
var params = {
TableName: "UsersTable",
Key: {
"emailId": _emailId,
"departmentId": _departmentId
}
};
docClient.get(params, function(err, data) {
if (!err) {
if (!data.items)
AddUserAndSendEmail("UsersTable", _emailId, _userName);
//The above function is being called twice but for the same user.
//It has a check so not inserting the same record twice but
//sending two mails to the same user.
}
});
}
res.end("success");
}
function AddUserAndSendEmail(tableName, emailId, _userName) {
var params = {
TableName: tableName,
Item: {
"emailId": emailId,
"departmentId": 101//Default Department
}
};
docClient.put(params, function(err, data) {
if (!err) {
//Send Email Code Here
} else {
console.log("error");
}
});
}
What could be the reason for this strange behavior? Really frustrated, I am about to give up on this.
1) Please note that DynamoDB is eventually consistent. If you insert the item and check whether the item exists immediately, it may not always find the item in the database.
This means the second iteration of the loop may not always find the first item inserted into the table.
2) If the item already exists in the table, the Put api will update the item and give successful response.
This means the Put will be successful for the same email id and department id in the second iteration because it updates the record if it is already present.
GetItem – The GetItem operation returns a set of Attributes for an
item that matches the primary key. The GetItem operation provides an
eventually consistent read by default. If eventually consistent reads
are not acceptable for your application, use ConsistentRead.
PutItem – Creates a new item, or replaces an old item with a new item
(including all the attributes). If an item already exists in the
specified table with the same primary key, the new item completely
replaces the existing item. You can also use conditional operators to
replace an item only if its attribute values match certain conditions,
or to insert a new item only if that item doesn’t already exist.
Based on the above points, there is a possibility to get two emails if you have same email id and department id in the array.
When I was trying to add payment to a sales order through web service API, I got "PX.Data.PXSetPropertyException: Error: An error occurred while processing the field Payment Ref. : CS Error #30: Cannot generate the next number for the AR Last Reference Number sequence..". The code is as follows:
AR302000Content AR302000 = context.AR302000GetSchema();
context.AR302000Clear();
List<Command> cmds = new List<Command>();
//add header info
cmds.Add(new Value { Value = " <NEW>", LinkedCommand = AR302000.PaymentSummary.ReferenceNbr });
cmds.Add(new Value { Value = "Payment", LinkedCommand = AR302000.PaymentSummary.Type, Commit = true });
cmds.Add(new Value { Value = "A123456", LinkedCommand = AR302000.PaymentSummary.Customer });
cmds.Add(new Value { Value = "MAIN", LinkedCommand = AR302000.PaymentSummary.Location });
cmds.Add(new Value { Value = "CreditCard", LinkedCommand = AR302000.PaymentSummary.PaymentMethod });
cmds.Add(new Value { Value = "300.00", LinkedCommand = AR302000.PaymentSummary.PaymentAmount});
cmds.Add(new Value { Value = "T123456", LinkedCommand = AR302000.PaymentSummary.PaymentRef, Commit = true });
cmds.Add(new Value { Value = "99000001", LinkedCommand = AR302000.PaymentSummary.CashAccount });
cmds.Add(AR302000.OrdersToApply.ServiceCommands.NewRow);
cmds.Add(new Value { Value = "00001234", LinkedCommand = AR302000.OrdersToApply.OrderNbr });
cmds.Add(new Value { Value = "SO", LinkedCommand = AR302000.OrdersToApply.OrderType });
cmds.Add(new Value { Value = "300.00", LinkedCommand = AR302000.OrdersToApply.OrderTotal });
cmds.Add(new Value { Value = "300.00", LinkedCommand = AR302000.OrdersToApply.AppliedToOrder });
cmds.Add(AR302000.Actions.Save);
AR302000Content[] AR302000content = context.AR302000Submit(cmds.ToArray());
I checked my "numbering sequences" configuration and it looks like the only payment related numbering sequence is "ARPAYMENT", which was correct and plus I believe this "ARPAYMENT" is for "ReferenceNbr" anyway.
There doesn't seem to be a numbering sequence for "PaymentRef", which seems to be what the error message was complaining about. Based on the "help" document, this "Payment Ref" should be able to accept a given string, which I did but seems not working...
Anybody knows what could be wrong?
I just found the problem - when I created payment, as part of the mandatory requirements, I assigned a cash account, which was associated with one of the "Payment method" - Credit Card, however, for some reason, in that cash account page, "Credit Card" payment method was specified as "AR - Suggest Next Number", which I have no idea about what it means, to be honest. After I unchecked that checkbox, everything became OK. I guess that checkbox means "Payment Ref" field on "Payment and Application" screen needs to be auto-generated, but it's really confusing as there is nowhere mentioning "Payment Ref.".
Anyway, the problem has been resolved and thanks for all of the responses!
I am working on a script for net suite. The point of the script is to check a sales order to make sure the credit limit is higher than the order total before actually approving the order. The script runs when the user clicks the "approve" button on the sales order. I successfully check to see if its a sales order and if the action type is approve. What I'm not understanding is how to stop the approval if the order is indeed over the credit limit. I tried setting the order status back to pending approval and submit the order but it doesn't work like that. The status is not getting set. There is also no cancel type of command.
function beforeApproveSalesOrder(type)
{
var recordType = nlapiGetRecordType();
var recordId = nlapiGetRecordId();
if (recordType == 'salesorder')
{
if (type == 'approve')
{
var recordId = nlapiGetRecordId();
nlapiLogExecution("debug", "Check Credit Limit Before Approval", "Found sales order with transaction type of approve.");
var salesOrderTotalCost = nlapiGetFieldValue('total');
var customer = nlapiGetFieldValue('entity');
var customerCreditLimit = queryCustomerCreditLimit(customer);
if (customerCreditLimit < salesOrderTotalCost)
{
nlapiSetFieldText('orderstatus', 'A');
nlapiLogExecution("debug", "Check Credit Limit Before Approval", "order status set");
nlapiSubmitRecord(recordId, true);
}
}
}
}
function queryCustomerCreditLimit( customer )
{
var filters = new Array();
filters[0] = new nlobjSearchFilter( 'internalid', 'null', 'is', customer);
var columns = new Array();
columns[0] = new nlobjSearchColumn( 'creditlimit' );
var searchresults = nlapiSearchRecord( 'customer', null, filters, columns );
var creditLimit = searchresults[ 0 ].getValue( 'creditlimit' );
return creditLimit;
}
You could throw an exception and thereby abort the submit action. In your beforeSubmit() function, you can have something like:
if (customerCreditLimit < salesOrderTotalCost){
throw nlapiCreateError('Error','Cannot Approve. Exceeds credit limit',true);
}
Your code works but you need to change nlapiSetFieldText('orderstatus', 'A'); to nlapiSetFieldValue('orderstatus', 'A');
I used it on a script that compares the total amount of an SO and changes the approval status if it has been increased.
With CR306000SetSchema and CR306000Submit, I can create a new Case. But if I want to get details of the particular case with its activities then. Any one has any idea.
We can use CR306000Export to get the data. Below code sample will get you the activity of particular case ID. For instance "000001" in example below.
string[][] export = context.CR306000Export
(
new CaseAPI.Command[]
{
new CaseAPI.Value
{
Value = "000001",
LinkedCommand = CR306000.CaseSummary.CaseID
},
CR306000.Activities.Type,
CR306000.Activities.Summary,
CR306000.Details.Description,
CR306000.Activities.Status,
CR306000.Activities.StartDate,
CR306000.CaseSummary.Owner
},
new CaseAPI.Filter[]
{
new CaseAPI.Filter
{
Field = CR306000.Activities.StartDate,
Condition = CaseAPI.FilterCondition.GreaterOrEqual,
Value = DateTime.Today
}
},
0, true, true
);
I have a scenario in node/express/mongoose where I have users who accumulate points, and when that point total crosses a certain threshold, they "level up" (think games with point-based levels).
I have created a custom setter on the points field that checks if the value has changed, and if so tries to update the level field. Levels are defined in another collection, but are saved as simple JSON objects when associated with user docs (hence the .lean() in the query). I did it this way vs a virtual field or population for efficiency.
Problem: this doesn't actually seem to update the user 'level' field when it should. What am I doing wrong?
// Define our user schema
var UserSchema = new mongoose.Schema({
...
points: {type: Number, default: 0, set: pointsChangeHandler},
level: {name: String, minPoints: Number, maxPoints: Number},
...
});
And the setter looks like so:
function goodPointsChangeHandler(newValue) {
var self = this;
if (newValue != self.goodPoints) {
console.log('Point change detected.');
// Find level that corresponds with new point total
Level.findOne({
'minPoints': {$lte : self.goodPoints},
'maxPoints': {$gt : self.goodPoints}}, '-_id').lean().exec(function(err, level) {
if (self.goodLevel == undefined || self.goodLevel.rank != level.rank) {
console.log('Level changed.');
self.goodLevel = level;
}
return newValue;
});
}
return newValue;
}
Based on #laggingreflex's comment, I tried modifying this within the scope of the model method (i.e. not in the Level.findOne() callback, and changes made that way were persisted without an explicit save() call.
Also, I had a pretty silly error where I was returning newValue from thefindOne` callback.. not sure what I was thinking there...
Long story short, and this may be obvious to node/express/mongoose experts, but you can modify fields other than the one whose setter method you're currently in, but the moment you find yourself in the callback of another async method, you'll have to do an explicit save() or your modifications to this will not be persisted.
So:
function myFieldSetterMethod(newValue) {
this.myField = "a";
this.myOtherField = "b";
return newValue;
// no save() necessary, this will update both fields
}
function myFieldSetterMethod(newValue) {
this.myField = "a";
SomeModel.findOne(..., function(err, doc) {
this.myOtherField = doc.somethingIWantFromThisDoc;
// now you'll need an explicit save
// this.save(...)
});
return newValue;
}