This commit is contained in:
2022-03-07 20:08:45 +00:00
parent cee78d5c3f
commit 73f8dbc3dc
18 changed files with 125 additions and 28 deletions

View File

@@ -68,11 +68,9 @@ Only Customers who are set to Active and have an email address set will be notif
### Duplicate or overlapping notifications ### Duplicate or overlapping notifications
You can set up multiple subscriptions for the same event to allow for alternative languages and other settings. Because of this, it is possible to set a combination of settings that results in the same customer being notified more than once of the same event. You can set up multiple subscriptions for the same event to allow for alternative languages and other settings. Because of this, it is possible to set a combination of settings that could result in the same customer being notified more than once of the same event.
For example if you have two notifications set up for the same event but with different language or time zone settings but choose tags in each subscription that end up matching the same Customer, that customer will receive two notifications. To prevent redundant excess notifications, AyaNova will only send *one* notification per event to the same Customer. If two notification subscriptions for the same event would apply to the same Customer, the subscription created first (lowest id number) will "win" and be the one used to make the delivery.
Use distinct and specific Customer notification tags to ensure that the appropriate notification event goes to the correct Customers without overlap. For example "workorder-completed-spanish", "workorder-completed-english" or whatever makes sense for your business but clearly separates the Customers into the appropriate groups for notification.
### Email address ### Email address

View File

@@ -143,7 +143,7 @@ namespace AyaNova.Api.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState)); return BadRequest(new ApiErrorResponse(ModelState));
string custTagsWhere = DataList.DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("acustomer.tags", customerTags, false); string custTagsWhere = DataList.DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("acustomer.tags", customerTags, false);
List<CustomerNotifySubscriptionWhoRecord> ret = new List<CustomerNotifySubscriptionWhoRecord>(); List<CustomerNotifySubscriptionWhoRecord> ret = new List<CustomerNotifySubscriptionWhoRecord>();

View File

@@ -1146,11 +1146,11 @@ namespace AyaNova.DataList
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
//
/// <summary> /// <summary>
/// Translate KPI tag filter to PostgreSQL friendly SQL criteria /// Translate tag filter to PostgreSQL friendly SQL criteria
/// </summary> /// </summary>
public static string KPITagFilterToSqlCriteria(string SqlColumnNameToFilter, List<string> sValue, bool bAny) ///
public static string TagFilterToSqlCriteriaHelper(string SqlColumnNameToFilter, List<string> sValue, bool bAny)
{ {
if(sValue.Count==0) return string.Empty; if(sValue.Count==0) return string.Empty;
//if it's an OR (any) query then need to build individual terms, if it's not then it's just an all or AND query and can pass through as is //if it's an OR (any) query then need to build individual terms, if it's not then it's just an all or AND query and can pass through as is

View File

@@ -1249,6 +1249,7 @@ namespace AyaNova.Biz
{ {
//# STATUS CHANGE (create new status) //# STATUS CHANGE (create new status)
{ {
//PERSONAL SUBSCCRIPTION
//Conditions: must match specific status id value and also tags below //Conditions: must match specific status id value and also tags below
//delivery is immediate so no need to remove old ones of this kind //delivery is immediate so no need to remove old ones of this kind
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync(); var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
@@ -1276,6 +1277,44 @@ namespace AyaNova.Biz
} }
}//quote status change event }//quote status change event
{
//PROXY CUSTOMER NOTIFICATION SUBSCRIPTION HANDLING
//can this customer even be delivered to?
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == QuoteInfo.CustomerId).Select(x => new { x.Active, x.Tags, x.EmailAddress }).FirstOrDefaultAsync();
if (custInfo != null && custInfo.Active && !string.IsNullOrWhiteSpace(custInfo.EmailAddress))
{
//Conditions: must match specific status id value and also tags below
//delivery is immediate so no need to remove old ones of this kind
//note order by id ascending so that only the oldest notification "wins" as per docs in case of overlap to same customer
var subs = await ct.CustomerNotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusChange && z.IdValue == oProposed.QuoteStatusId).OrderBy(z=>z.Id).ToListAsync();
foreach (var sub in subs)
{
//Object tags must match and Customer tags must match
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(QuoteInfo.Tags, sub.Tags) && NotifyEventHelper.ObjectHasAllSubscriptionTags(custInfo.Tags, sub.CustomerTags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.QuoteStatusChange,
UserId = sub.UserId,
AyaType = AyaType.Quote,
ObjectId = oProposed.QuoteId,
NotifySubscriptionId = sub.Id,
Name = $"{QuoteInfo.Serial.ToString()} - {qos.Name}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
break;//we have a match no need to process any further subs for this event
}
}
}
}//quote status change event
//# STATUS AGE //# STATUS AGE
{ {
//QuoteStatusAge = 29,//* Quote STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set //QuoteStatusAge = 29,//* Quote STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set

View File

@@ -29,7 +29,7 @@ AuthorizationRoles.TechRestricted;
{ {
var custtags = options.Criteria["custtags"].ToObject<List<string>>(); var custtags = options.Criteria["custtags"].ToObject<List<string>>();
bool custtagsany = options.Criteria["custtagsany"].ToObject<bool>(); bool custtagsany = options.Criteria["custtagsany"].ToObject<bool>();
string custTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("acustomer.tags", custtags, custtagsany); ; string custTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("acustomer.tags", custtags, custtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT ACUSTOMERSERVICEREQUEST.DATEREQUESTED, SELECT ACUSTOMERSERVICEREQUEST.DATEREQUESTED,

View File

@@ -62,8 +62,8 @@ namespace AyaNova.KPI
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT distinct(AWORKORDER.ID), AWORKORDER.SERIAL, SELECT distinct(AWORKORDER.ID), AWORKORDER.SERIAL,

View File

@@ -45,7 +45,7 @@ AuthorizationRoles.Accounting;
var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp); var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp);
var wotags = options.Criteria["wotags"].ToObject<List<string>>(); var wotags = options.Criteria["wotags"].ToObject<List<string>>();
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);

View File

@@ -47,8 +47,8 @@ AuthorizationRoles.Accounting;
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT COUNT(AWORKORDER.ID) Y, SELECT COUNT(AWORKORDER.ID) Y,

View File

@@ -50,9 +50,9 @@ AuthorizationRoles.Accounting;
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string techTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("auser.tags", techtags, techtagsany); string techTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("auser.tags", techtags, techtagsany);
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;

View File

@@ -40,8 +40,8 @@ namespace AyaNova.KPI
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
select SUM(AWORKORDERITEMLABOR.serviceratequantity) y, date_trunc('{interval}',AWORKORDERITEMLABOR.servicestopdate) x select SUM(AWORKORDERITEMLABOR.serviceratequantity) y, date_trunc('{interval}',AWORKORDERITEMLABOR.servicestopdate) x

View File

@@ -30,8 +30,8 @@ namespace AyaNova.KPI
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT distinct(AWORKORDER.ID), SELECT distinct(AWORKORDER.ID),

View File

@@ -25,8 +25,8 @@ namespace AyaNova.KPI
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT distinct(AWORKORDER.ID), SELECT distinct(AWORKORDER.ID),

View File

@@ -45,7 +45,7 @@ namespace AyaNova.KPI
var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp); var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp);
var wotags = options.Criteria["wotags"].ToObject<List<string>>(); var wotags = options.Criteria["wotags"].ToObject<List<string>>();
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
SELECT COUNT(AWORKORDER.ID) Y,DATE_TRUNC('{interval}', AWORKORDER.createddate) X, aworkorder.laststatusid Z SELECT COUNT(AWORKORDER.ID) Y,DATE_TRUNC('{interval}', AWORKORDER.createddate) X, aworkorder.laststatusid Z

View File

@@ -45,7 +45,7 @@ namespace AyaNova.KPI
var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp); var dateWhere = DataListSqlFilterCriteriaBuilder.DataFilterToColumnCriteria("aworkorder.createddate", UiFieldDataType.DateTime, "no-operator", timeSpan, options.ClientTimeStamp);
var wotags = options.Criteria["wotags"].ToObject<List<string>>(); var wotags = options.Criteria["wotags"].ToObject<List<string>>();
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
_dataQuery = @$"SELECT row_to_json(t) as res from ( _dataQuery = @$"SELECT row_to_json(t) as res from (
WITH SUBQ AS WITH SUBQ AS

View File

@@ -45,8 +45,8 @@ AuthorizationRoles.TechRestricted;
bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>(); bool wotagsany = options.Criteria["wotagsany"].ToObject<bool>();
var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>(); var woitemtags = options.Criteria["woitemtags"].ToObject<List<string>>();
bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>(); bool woitemtagsany = options.Criteria["woitemtagsany"].ToObject<bool>();
string woTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorder.tags", wotags, wotagsany); string woTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorder.tags", wotags, wotagsany);
string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.KPITagFilterToSqlCriteria("aworkorderitem.tags", woitemtags, woitemtagsany); ; string woItemTagsWhere = DataListSqlFilterCriteriaBuilder.TagFilterToSqlCriteriaHelper("aworkorderitem.tags", woitemtags, woitemtagsany); ;

View File

@@ -128,6 +128,7 @@ namespace AyaNova.Models
public virtual DbSet<ViewScheduleWorkOrder> ViewScheduleWorkOrder { get; set; } public virtual DbSet<ViewScheduleWorkOrder> ViewScheduleWorkOrder { get; set; }
public virtual DbSet<CustomerNotifySubscription> CustomerNotifySubscription { get; set; } public virtual DbSet<CustomerNotifySubscription> CustomerNotifySubscription { get; set; }
public virtual DbSet<CustomerNotifyEvent> CustomerNotifyEvent { get; set; }

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace AyaNova.Models
{
//Customer notification event
public class CustomerNotifyEvent
{
public long Id { get; set; }
public uint Concurrency { get; set; }
[Required]
public DateTime Created { get; set; }
public AyaType AyaType { get; set; }
public long ObjectId { get; set; }
[Required]
public string Name { get; set; }//object name or closest equivalent for display
[Required]
public NotifyEventType EventType { get; set; }
[Required]
public long CustomerId { get; set; }
[Required]
public long CustomerNotifySubscriptionId { get; set; }//source subscription that triggered this event to be created
public decimal DecValue { get; set; }
//date of the event actually occuring, e.g. WarrantyExpiry date. Compared with subscription to determine if deliverable or not
public DateTime EventDate { get; set; }
public string Message { get; set; }
public CustomerNotifyEvent()
{
Created = EventDate = DateTime.UtcNow;
// IdValue = 0;
DecValue = 0;
AyaType = AyaType.NoType;
ObjectId = 0;
Name = string.Empty;
}
public override string ToString()
{
return JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.None);
}
//linked entity
// public NotifySubscription NotifySubscription { get; set; }
// public User User { get; set; }
}//eoc
}//eons

View File

@@ -1213,6 +1213,9 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
+ "idvalue BIGINT NOT NULL, decvalue DECIMAL(38,18) NOT NULL, agevalue INTERVAL NOT NULL, " + "idvalue BIGINT NOT NULL, decvalue DECIMAL(38,18) NOT NULL, agevalue INTERVAL NOT NULL, "
+ "linkreportid BIGINT, template TEXT NOT NULL, tags VARCHAR(255) ARRAY)"); + "linkreportid BIGINT, template TEXT NOT NULL, tags VARCHAR(255) ARRAY)");
await ExecQueryAsync("CREATE TABLE acustomernotifyevent (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created TIMESTAMPTZ NOT NULL, "
+ "ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, name TEXT NOT NULL, eventtype INTEGER NOT NULL, customernotifysubscriptionid BIGINT NOT NULL REFERENCES acustomernotifysubscription(id) ON DELETE CASCADE, "
+ "customerid BIGINT NOT NULL REFERENCES acustomer (id) ON DELETE CASCADE, eventdate TIMESTAMPTZ NOT NULL, decvalue DECIMAL(38,18) NULL, message TEXT)");