This also substantially cleans up JSON serialization in the APIs, and also adds a Docker build for the client.
IR75ZMX32SFFMDNV2I2L22X5JTWCOC4UUBCSPU7S6VHR6HFV6ADQC
5SBSBFLSDRLVSWPBVP4SWOKNAXWIZL5YR646VFCBRUAG2C5QOGUQC
4354Y4PECM6BOEYIKW2L6WP6ULDIQK2KMNLORWPVKHKQTHUI6CRQC
MU6WOCCJQWG4A5NLD3GBFATCE3SRE3QQCYXYH6WIKSGLHQOOBVRAC
B6HWAPDPXIWH7CHK5VLMWLL6EQN6NOFZEFYO47BPUY2ZO4SL7VDAC
SOIAMXLWIB5RIEMKXUFMBSE2SKQQTMHYSW3DKUX6GEV4VNOQVHAQC
LEINLS3X55PB6TSCNC5RVMDMV56XHTV4MNDUC42H7DDFMPDYUNTAC
AWWC6P5ZVFDQHX3EAYDG4DKTUZ6A5LHQAV3NIUO3VP6FM7JKPK5AC
IPG33FAWXGEQ2PO6OXRT2PWWXHRNMPVUKKADL6UKKN5GD2CNZ25AC
AL37SVTCKRSG4HG2PCYK5Z7QSIZZH5JHH4Q2VLMXFAXSAQRFFG4QC
64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC
RB2ETNIFLQUA6OA66DAEOXZ25ENMQGNKX5CZRSKEYHTD6BQ6NTFQC
2J37EVJMX255K3XEJHTZGRPEIRMAQ62JQWOA7JU3YTZUB6PUPWVQC
RSF6UAJKG7CEKILSVXI6C4YZXY7PIYZM2EMA2IXKQ7SADKNVSH7QC
OUR4PAOTXXKXQPMAR5TIYX7MBRRJS2WVTZS7SN4SOGML7SPJIJGQC
JXG3FCXYBDKMUD77DOM7RCIJYKB7BILC43OHHDZBE7YQRGAMUCCAC
QMEYU4MWLTSWPWEEOFRLK2IKE64BY3V5X73323WPLCGPP3TPDYGAC
QU5FW67RGCWOWT2YFM4NYMJFFHWIRPQANQBBAHBKZUY7UYMCSIMQC
ZIG57EE6RB22JGB3CT33EN2HVYCHCXBT5GROBTBMBLEMDTGQOOBQC
NJNMO72S7VIUV22JXB4IFPZMHWTJAOTP6EC6Z4QSKYIROSXT52MQC
BFZN4SUAGYNFFYVAP36BAX32DMO622PK4EPEVQQEAGC2IHTEAAPQC
WRPIYG3EUHZR6N6T74ZXZDXATRMIRLXAQ24UNUNSVTVYGMT2VDSQC
J6S23MDGHVSCVVIRB6XRNSY3EGTDNWFJHV7RYLIEHBUK5KU63CFQC
4ZLEDBK7VGLKFUPENAFLUJYNFLKFYJ3TREPQ7P6PKMYGJUXB55HQC
GMYPBCWEB6NKURRILAHR3TJUKDOGR2ZMK5I6MS6P5G2LAGH36P3QC
MJDIMD5BQEBC265AQAGYE2K6EHHS7ZMZY3I6WE5MCDSTA2E2VY7AC
SQ7UMLN5WCPHIF66RO4UQVX6RSNRRZBOVZP7HEMSKP7VO6YNQPRAC
NSRSSSTRMJPPUYQANYDWGI5D3NVM6RQEVZCDUUNQAOL3OWQTD27AC
M4PWY5RUV72AEDCNC4O7UKBPHBIACR4354YTSC3SUZGWFV5UBJBQC
JUFBTX45TKSZMB2D4CGNB73UYM5FXAV2QMKIHBSMHEQDAECYP7HQC
IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC
X3ES7NUA42D2BF7CQDDKXM5CLMVCYA3H5YU5KXLPTGDBFPE2LNVAC
QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC
EFSXYZPOGA5M4DN65IEIDO7JK6U34DMQELAPHOIL2UAT6HRC66BAC
2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC
O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC
HMDM3B557TO5RYP2IGFFC2C2VN6HYZTDQ47CJY2O37BW55DSMFZAC
EQXRXRZDYCM7BDAVBOXQYPG6C7IJT3OFGNIXCDAHJJBRKAXNGL7AC
Z7KS5XHHC6PAMTVBHXY7KUSS3BWAOU6FSYIITUCFOOJZU4OUJHBAC
4R7XIYK3BP664CO3YJ2VM64ES2JYN27UTQG5KS34OTEPAIODSZLQC
7XN3I3QJHYMKU2DCUXX34WQMSJ4ZJOWW7FME34EANO3E5W4Q632AC
7KZP4RHZ3QSYTPPQ257A65Z5UPX44TF2LAI2U5EMULQCLDCEUK2AC
7DBNV3GV773FH5ZLQWFX4RBOS4Q3CIK2RYZNNABY3ZOETYZCXRNQC
RPAJLHMTUJU4AYNBOHVGHGGB4NY2NLY3BVPYN5FMWB3ZIMAUQHCQC
SCXG6TJWYIPRUMT27KGKIIF6FYKTUTY74UNZ2FQTT63XZ6HIF3AAC
7HPY3QPFPN35PSPUBVNW2GTFB3CBQZBST4J2BAVJ7QMXLIUN52JAC
N4NDAZYTLSI2W22KT3SYXL257DBMSH3UT2BXOYM7LH7FSZAY4RLAC
A6HKMINBNGQLLX4QJMYWKQ4JAEHVJ4HIRVDKPPDI3FJUO2AAB7OQC
W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC
EW2XN7KUMCAQNVFJJ5YTAVDZCPHNWDOEDMRFBUGLY6IE2HKNNX5AC
XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC
4IQVQL4TS35GL2GYZJG254TKJLL5EHMRSFT77Z4VTRZIG2TMBM3QC
GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC
UWMGUJOW5X5HQTS76T2FD7MNAJF7SESPQVU5FDIZO52V75TT2X6AC
7VGYLTMURLVSVUYFW7TCRZTDQ6RE2EPSPPA43XKHDOBFWYVVSJHQC
ASF3UPJLCX7KIUCNJD5KAXSPDXCUEJHLL4HBRTRUBPCW73IXOCWQC
Z3MK2PJ5U222DXRS22WCDHVPZ7HVAR3HOCUNXIGX6VMEPBQDF6PQC
POX3UAMTGCNS3SU5I6IKDNCCSHEAUSFBZ3WCMQ3EXKPNXETOI6BAC
RN7EI6INGUUHGMNY5RU3NH56WPLRZY5ZMYDNFMNE3TGV4ESFLQIAC
O722AOKEWXWJPRHGJREU6QPW7HEFPPRETZIAADZ2RMAXHARCNEKAC
O227CEAV7BTKSE3SSC7XHC5IWEBXZL2AOOKJMBMOOFNTLINBLQMAC
WAIX6AGNDVJOKTWZ7OP7QOYSJHAJSX5EOWXZHOAO2IG6ALWUCJ6QC
DFOBMSAODB3NKW37B272ZXA2ML5HMIH3N3C4GT2DPEQS7ZFK4SNAC
Q5X5RYQLP5K7REYD6VLHOKC4W36ZELJYA45V6YFKTD5S6MPN3NDQC
UILI6PILCRDPZ3XYA54LGIGPSU7ERWNHCE7R3CE64ZEC7ONOEMOQC
BWN72T44GRRZ6K2OPN56FTLNEB7J7AGC7T2U5HSMLEKUPGJP2NUAC
NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC
TLQ72DSJD7GGPWN6HGBHAVPBRQFKEQ6KSK43U7JWWID4ZWAF47JAC
GKGVYBZGPJXO7N7GLHLRNYQPXFHBQSNQN53OKRFCXLQEYDTC5I4QC
5DRIWGLUKMQZU2ZPBXSTLAWJKAMOD5YXAHM5LEDQHDFGYYLHWCDQC
NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC
4QX5E5ACVN57KJLCWOM4JEI6JSV4XZNCWVYPOTKSOMUW3SOMCNJAC
U256ZALIPTVWLNACYPIMWLNEYDQWP7CHF4Y4CGMILQTONJHMGQVQC
KNSI575VAW6HRCZYXOEPQ4DTSML4EORML5MV4DJBRKE7TXCPS4EAC
Y35QCWYW2OTZ27ZVTH2BA3XJTUCJ2WMLKU32ZCOCDY3AW7TIZXRAC
A2J7B4SCCJYKQV3G2LDHEFNE2GUICO3N3Y5FKF4EUZW5AG7PTDWAC
2WOOGXDHVQ6L2MQYUTLJ6H6FVSQNJN6SMJL5DG7HAHFYPJLRT2SAC
PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC
HALRDT2F22DAQ54M334YSKD2XJCAXDXEZPDKGMOWKH7XELBAS6MAC
4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC
BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC
I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC
Z24SZOGZJLDTDTGWH7M25RYQ7MYSU52ZLFWJ2PSQFTMK4J35PIWAC
EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC
ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC
docker tag aftok/aftok-client:latest aftok/aftok-client:$(VERSION)
docker push docker.io/aftok/aftok-client:latest
docker push docker.io/aftok/aftok-client:$(VERSION)
FROM ubuntu:focal
MAINTAINER Kris Nuttycombe <kris@aftok.com>
ENV LANG C.UTF-8
ENV TZ America/Denver
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Install build tools & library dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libtinfo5 nodejs npm netbase
RUN apt-get install -y --no-install-recommends ca-certificates
RUN update-ca-certificates
RUN mkdir -p /opt/aftok/client
WORKDIR /opt/aftok/client
ADD ./client/package.json /opt/aftok/client/package.json
RUN npm install
ENV PATH="./node_modules/.bin:${PATH}"
# Add static assets
ADD ./aftok.com/src/assets /opt/aftok/aftok.com/src/assets
ADD ./client/dev /opt/aftok/client/dev
RUN mkdir -p /opt/aftok/client/prod && \
ln -s /opt/aftok/aftok.com/src/assets /opt/aftok/client/prod/assets
# Add purescript build config & sources
ADD ./client/spago.dhall /opt/aftok/client/spago.dhall
ADD ./client/packages.dhall /opt/aftok/client/packages.dhall
ADD ./client/src /opt/aftok/client/src
RUN npm run build-prod
# Add dist-volume directory for use with docker-compose sharing
# of client executables via volumes.
ADD ./docker/aftok-client-cp.sh /opt/aftok/
RUN chmod 700 /opt/aftok/aftok-client-cp.sh
RUN mkdir /opt/aftok/client/dist-volume
instance decodeJsonEvent :: DecodeJson (Event' String) where
parseEventFields :: Object Json -> Either String (Event String)
parseEventFields obj = do
ev <- obj .: "event"
start' <- traverse (_ .: "eventTime") =<< ev .:? "start"
stop' <- traverse (_ .: "eventTime") =<< ev .:? "stop"
note "Only 'stop' and 'start' events are supported." $
(StartEvent <$> start') <|>
(StopEvent <$> stop')
instance eventDecodeJSON :: DecodeJson (Event String) where
decodeJson = parseEventFields <=< decodeJson
newtype KeyedEvent i = KeyedEvent
{ eventId :: String
, event :: Event i
}
keyedEvent :: forall i. String -> Event i -> KeyedEvent i
keyedEvent eid ev = KeyedEvent { eventId: eid, event: ev }
eventId :: forall i. KeyedEvent i -> String
eventId (KeyedEvent xs) = xs.eventId
event :: forall i. KeyedEvent i -> Event i
event (KeyedEvent xs) = xs.event
derive instance keyedEventFunctor :: Functor KeyedEvent
instance keyedEventFoldable :: Foldable KeyedEvent where
foldr f b = foldr f b <<< event
foldl f b = foldl f b <<< event
foldMap = foldMapDefaultR
instance keyedEventTraversable :: Traversable KeyedEvent where
traverse f (KeyedEvent xs) = (\ev -> KeyedEvent { eventId: xs.eventId, event: ev }) <$> traverse f xs.event
sequence = traverse identity
instance keyedEventDecodeJson :: DecodeJson (KeyedEvent String) where
event <- obj .: "event"
start' <- traverse (_ .: "eventTime") =<< event .:? "start"
stop' <- traverse (_ .: "eventTime") =<< event .:? "stop"
note "Only 'stop' and 'start' events are supported." $ (StartEvent <$> start') <|> (StopEvent <$> stop')
keyedEvent <$> obj .: "eventId" <*> parseEventFields obj
instance intervalFoldable :: Foldable Interval' where
foldr f b (Interval i) = f i.start (f i.end b)
foldl f b (Interval i) = f (f b i.start) i.end
instance intervalFoldable :: Foldable Interval where
foldr f b (Interval i) = f i.start (f i.end b)
foldl f b (Interval i) = f (f b i.start) i.end
event <- withExceptT LogFailure $ parseDatedResponse response
case event of
StartEvent t -> pure t
StopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."
kev <- withExceptT LogFailure $ parseDatedResponse response
case event kev of
StartEvent _ -> pure kev
StopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."
import Aftok.Api.Timeline (TimelineError, Interval'(..), Interval, TimeSpan, start, end, interval)
import Aftok.Api.Timeline
( TimelineError,
Event(..),
Interval(..),
TimeInterval,
KeyedEvent,
TimeSpan,
start, end, interval,
event, eventTime, keyedEvent
)
data TimelineEvent
= LoggedEvent (KeyedEvent Instant)
| PhantomEvent Instant
instance showTimelineEvent :: Show TimelineEvent where
show = case _ of
LoggedEvent kev -> "Real event at " <> show (event kev)
PhantomEvent i -> "Phantom at " <> show i
tlEventTime :: TimelineEvent -> Instant
tlEventTime = case _ of
LoggedEvent kev -> eventTime <<< event $ kev
PhantomEvent i -> i
, logStart :: ProjectId -> m (Either TimelineError Instant)
, logEnd :: ProjectId -> m (Either TimelineError Instant)
, listIntervals :: ProjectId -> TimeSpan -> m (Either TimelineError (Array Interval))
, getLatestEvent :: ProjectId -> m (Either TimelineError (Maybe TL.Event))
, logStart :: ProjectId -> m (Either TimelineError (KeyedEvent Instant))
, logEnd :: ProjectId -> m (Either TimelineError (KeyedEvent Instant))
, listIntervals :: ProjectId -> TimeSpan -> m (Either TimelineError (Array (Interval (KeyedEvent Instant))))
, getLatestEvent :: ProjectId -> m (Either TimelineError (Maybe (KeyedEvent Instant)))
updateStart :: Instant -> TimelineState -> TimelineState
updateStart t s =
s { active = s.active <|> Just (TL.interval t t) }
updateStart :: KeyedEvent Instant -> TimelineState -> TimelineState
updateStart ev s =
s { active = s.active <|> Just (TL.interval (LoggedEvent ev) (PhantomEvent <<< eventTime <<< event $ ev)) }
updateStop system t st = do
newHistory <- join <$> traverse (\i -> runMaybeT $ toHistory system [TL.interval (start i) t]) st.active
updateStop system ev st = do
let updateHistory i = runMaybeT $ toHistory system [TL.interval (start i) (LoggedEvent ev)]
newHistory <- join <$> traverse updateHistory st.active
, logStart: \_ -> Right <$> liftEffect now
, logEnd: \_ -> Right <$> liftEffect now
, logStart: \_ -> Right <<< keyedEvent "" <<< StartEvent <$> liftEffect now
, logEnd: \_ -> Right <<< keyedEvent "" <<< StopEvent <$> liftEffect now
end <- MaybeT <<< pure $ fromDateTime <$> DT.adjust (Days 1.0) (toDateTime start)
nextNoon <- MaybeT <<< pure $
fromDateTime <$> (
DT.adjust (Hours 12.0) <=< DT.adjust (Days 1.0) $
(toDateTime start)
)
Tuple _ end <- MaybeT $ system.dateFFI.midnightLocal nextNoon
--lift <<< system.log $ "Splitting interval " <> show i
dayBounds@(Tuple date bounds) <- localDayBounds system (start i)
split <- if end i < (end bounds)
then do
pure [Tuple date { dayBounds: bounds, loggedIntervals: [i] }]
else do
let firstFragment = [ Tuple date { dayBounds: bounds
, loggedIntervals: [interval (start i) (end bounds)]
} ]
append firstFragment <$> splitInterval system (interval (end bounds) (end i))
lift <<< system.log $ "Splitting interval " <> show i
-- day bounds are based on the start event.
Tuple date bounds <- localDayBounds system (tlEventTime $ start i)
lift <<< system.log $ "Splitting on day bounds: " <> show (start bounds) <> " to " <> show (end bounds)
split <- if tlEventTime (end i) < end bounds
then do
lift <<< system.log $ "Split complete"
pure [Tuple date { dayBounds: bounds, loggedIntervals: [i] }]
else do
let splitEvent = PhantomEvent (end bounds)
currInterval = Tuple date { dayBounds: bounds, loggedIntervals: [interval (start i) splitEvent] }
nextInterval = interval splitEvent (end i)
lift <<< system.log $ "Split required; first fragment: " <> show currInterval <> "; next interval: " <> show nextInterval
cons currInterval <$> splitInterval system nextInterval
#!/bin/bash
echo "Copying client build artifacts to mounted volume..."
cp -r /opt/aftok/client/dist/* /opt/aftok/client/dist-volume
echo "Client copy complete. The container will now shut down."
zcashd:
image: electriccoinco/zcashd:v4.0.0
container_name: aftok-zcashd
expose:
- "18232"
ports:
- "18233:18233"
volumes:
- type: bind
source: ./local/zcashd/zcash-data
target: /srv/zcashd/.zcash
- type: bind
source: ./local/zcashd/zcash-params
target: /srv/zcashd/.zcash-params
read_only: true
networks:
- back-tier
# zcashd:
# image: electriccoinco/zcashd:v4.0.0
# container_name: aftok-zcashd
# expose:
# - "18232"
# ports:
# - "18233:18233"
# volumes:
# - type: bind
# source: ./local/zcashd/zcash-data
# target: /srv/zcashd/.zcash
# - type: bind
# source: ./local/zcashd/zcash-params
# target: /srv/zcashd/.zcash-params
# read_only: true
# networks:
# - back-tier
logEntryParser
[sql| SELECT credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time,
event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time <= ?
ORDER BY event_time DESC
LIMIT ?
|]
keyedLogEntryParser
[sql| SELECT id, credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time,
event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time <= ?
AND replacement_id IS NULL
ORDER BY event_time DESC
LIMIT ?
|]
logEntryParser
[sql| SELECT credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ?
AND event_time >= ? AND event_time <= ?
ORDER BY event_time DESC
LIMIT ?
|]
keyedLogEntryParser
[sql| SELECT id, credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ?
AND replacement_id IS NULL
AND event_time >= ? AND event_time <= ?
ORDER BY event_time DESC
LIMIT ?
|]
logEntryParser
[sql| SELECT credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time >= ?
ORDER BY event_time DESC
LIMIT ?
|]
keyedLogEntryParser
[sql| SELECT id, credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time >= ?
AND replacement_id IS NULL
ORDER BY event_time DESC
LIMIT ?
|]
logEntryParser
[sql| SELECT credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ?
ORDER BY event_time DESC
LIMIT ?
|]
keyedLogEntryParser
[sql| SELECT id, credit_to_type,
credit_to_account, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ?
AND replacement_id IS NULL
ORDER BY event_time DESC
LIMIT ?
|]
amendEvent :: EventId -> EventAmendment -> DBM AmendmentId
amendEvent (EventId eid) = \case
(TimeChange mt t) ->
pinsert
AmendmentId
[sql| INSERT INTO event_time_amendments
(event_id, amended_at, event_time)
VALUES (?, ?, ?) RETURNING id |]
(eid, fromThyme $ mt ^. _ModTime, fromThyme t)
(CreditToChange mt c@(CreditToAccount acctId)) ->
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_account)
VALUES (?, ?, ?, ?) RETURNING id |]
(eid, fromThyme $ mt ^. _ModTime, creditToName c, acctId ^. _AccountId)
(CreditToChange mt c@(CreditToProject pid)) ->
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_project_id)
VALUES (?, ?, ?, ?) RETURNING id |]
(eid, fromThyme $ mt ^. _ModTime, creditToName c, pid ^. _ProjectId)
(CreditToChange mt c@(CreditToUser uid)) ->
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_user_id)
VALUES (?, ?, ?, ?) RETURNING id |]
(eid, fromThyme $ mt ^. _ModTime, creditToName c, uid ^. _UserId)
(MetadataChange mt v) ->
pinsert
AmendmentId
[sql| INSERT INTO event_metadata_amendments
(event_id, amended_at, event_metadata)
VALUES (?, ?, ?) RETURNING id |]
(eid, fromThyme $ mt ^. _ModTime, v)
pure $ workIndex logEntries
pure $ workIndex (view logEntry) logEntries
amendEvent :: ProjectId -> UserId -> KeyedLogEntry -> EventAmendment -> DBM (EventId, AmendmentId)
amendEvent pid uid kle amendment = ptransact $ do
(amendId, replacement, amend_t :: Text) <- amend
newEventId <- createEvent pid uid (replacement ^. logEntry)
void $
pexec
[sql| UPDATE work_events
SET replacement_id = ?, amended_by_id = ?, amended_by_type = ?
WHERE id = ? |]
(newEventId ^. _EventId, amendId ^. _AmendmentId, amend_t, kle ^. workId . _EventId)
pure (newEventId, amendId)
where
amend = case amendment of
(TimeChange mt t) -> do
aid <-
pinsert
AmendmentId
[sql| INSERT INTO event_time_amendments
(work_event_id, amended_at, event_time)
VALUES (?, ?, ?) RETURNING id |]
(kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, fromThyme t)
pure (aid, set (logEntry . event . eventTime) t kle, "amend_event_time")
(CreditToChange mt c@(CreditToAccount acctId)) -> do
aid <-
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(work_event_id, amended_at, credit_to_type, credit_to_account)
VALUES (?, ?, ?, ?) RETURNING id |]
(kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, acctId ^. _AccountId)
pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")
(CreditToChange mt c@(CreditToProject cpid)) -> do
aid <-
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(work_event_id, amended_at, credit_to_type, credit_to_project_id)
VALUES (?, ?, ?, ?) RETURNING id |]
(kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, cpid ^. _ProjectId)
pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")
(CreditToChange mt c@(CreditToUser cuid)) -> do
aid <-
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(work_event_id, amended_at, credit_to_type, credit_to_user_id)
VALUES (?, ?, ?, ?) RETURNING id |]
(kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, cuid ^. _UserId)
pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")
(MetadataChange mt v) -> do
aid <-
pinsert
AmendmentId
[sql| INSERT INTO event_metadata_amendments
(work_event_id, amended_at, event_metadata)
VALUES (?, ?, ?) RETURNING id |]
(kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, v)
pure (aid, set (logEntry . eventMeta) (Just v) kle, "amend_metadata")
AmendEvent :: EventId -> EventAmendment -> DBOp AmendmentId
FindEvent :: EventId -> DBOp (Maybe KeyedLogEntry)
FindEvents :: ProjectId -> UserId -> RangeQuery -> Limit -> DBOp [LogEntry]
ReadWorkIndex :: ProjectId -> DBOp WorkIndex
AmendEvent :: ProjectId -> UserId -> KeyedLogEntry -> EventAmendment -> DBOp (EventId, AmendmentId)
FindEvent :: EventId -> DBOp (Maybe (ProjectId, UserId, KeyedLogEntry))
FindEvents :: ProjectId -> UserId -> RangeQuery -> Limit -> DBOp [KeyedLogEntry]
ReadWorkIndex :: ProjectId -> DBOp (WorkIndex KeyedLogEntry)
ev <- findEvent eid
let act = AmendEvent eid a
forbidden = raiseOpForbidden uid UserNotEventLogger act
missing = raiseSubjectNotFound act
maybe
missing
(\(_, uid', _) -> if uid' == uid then liftdb act else forbidden)
ev
evMay <- findEvent eid
maybe missing saveAmendment evMay
where
missing = raiseSubjectNotFound (FindEvent eid)
saveAmendment (pid, uid', le) =
let act = AmendEvent pid uid le a
in if uid' == uid
then liftdb act
else raiseOpForbidden uid UserNotEventLogger act
idJSON :: forall a. Text -> Getter a UUID -> a -> Value
idJSON t l a = v1 $ obj [t .= idValue l a]
qdbJSON :: Text -> Getter a UUID -> Getter a Value -> a -> Value
qdbJSON name _id _value x =
v1 $ obj [(name <> "Id") .= idValue _id x, name .= (x ^. _value)]
projectIdJSON :: ProjectId -> Value
projectIdJSON = idJSON "projectId" _ProjectId
projectJSON :: P.Project -> Value
projectJSON p =
v1 $
obj
[ "projectName" .= (p ^. P.projectName),
"inceptionDate" .= (p ^. P.inceptionDate),
"initiator" .= (p ^. P.initiator . _UserId)
]
qdbProjectJSON :: (ProjectId, P.Project) -> Value
qdbProjectJSON = qdbJSON "project" (_1 . _ProjectId) (_2 . L.to projectJSON)
auctionIdJSON :: A.AuctionId -> Value
auctionIdJSON = idJSON "auctionId" A._AuctionId
auctionJSON :: A.Auction Amount -> Value
auctionJSON x =
v1 $
obj
[ "projectId" .= idValue (A.projectId . _ProjectId) x,
"initiator" .= idValue (A.initiator . _UserId) x,
"name" .= (x ^. A.name),
"description" .= (x ^. A.description),
"raiseAmount" .= (x ^. (A.raiseAmount . to amountJSON)),
"auctionStart" .= (x ^. A.auctionStart),
"auctionEnd" .= (x ^. A.auctionEnd)
]
bidIdJSON :: A.BidId -> Value
bidIdJSON pid = v1 $ obj ["bidId" .= (pid ^. A._BidId)]
identifiedJSON :: Text -> Getter a UUID -> Getter a Value -> a -> Value
identifiedJSON name _id _value x =
object [(name <> "Id") .= idValue _id x, name .= (x ^. _value)]
v2 $ obj ["creditToProject" .= projectIdJSON pid]
parseCreditTo :: Value -> Parser CreditTo
parseCreditTo = unversion "CreditTo" $ \case
(Version 2 0) -> parseCreditToV2
ver -> badVersion "EventAmendment" ver
object ["creditToProject" .= idValue _ProjectId pid]
-- Payouts
--
payoutsJSON :: FractionalPayouts -> Value
payoutsJSON (Payouts m) =
v2 $
let payoutsRec :: (CreditTo, Rational) -> Value
payoutsRec (c, r) =
object ["creditTo" .= creditToJSON c, "payoutRatio" .= r, "payoutPercentage" .= (fromRational @Double r * 100)]
in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]
parsePayoutsJSON :: Value -> Parser FractionalPayouts
parsePayoutsJSON = unversion "Payouts" $ p
where
p :: Version -> Object -> Parser FractionalPayouts
p (Version 2 0) val =
let parsePayoutRecord x =
(,)
<$> (parseCreditToV2 =<< (x .: "creditTo"))
<*> (x .: "payoutRatio")
in Payouts
. MS.fromList
<$> (traverse parsePayoutRecord =<< parseJSON (Object val))
p ver x = badVersion "Payouts" ver x
--
workIndexJSON :: WorkIndex -> Value
workIndexJSON (WorkIndex widx) =
v2 $
obj ["workIndex" .= fmap widxRec (MS.assocs widx)]
where
widxRec :: (CreditTo, NonEmpty Interval) -> Value
widxRec (c, l) =
object
[ "creditTo" .= creditToJSON c,
"intervals" .= (intervalJSON <$> L.toList l)
]
eventIdJSON :: EventId -> Value
eventIdJSON = idJSON "eventId" _EventId
logEventJSON' :: LogEvent -> Value
logEventJSON' ev =
object [eventName ev .= object ["eventTime" .= (ev ^. eventTime)]]
logEntryJSON :: LogEntry -> Value
logEntryJSON le = v2 $ obj (logEntryFields le)
logEntryFields :: LogEntry -> [Pair]
logEntryFields (LogEntry c ev m) =
[ "creditTo" .= creditToJSON c,
"event" .= logEventJSON' ev,
"eventMeta" .= m
]
amendmentIdJSON :: AmendmentId -> Value
amendmentIdJSON = idJSON "amendmentId" _AmendmentId
createSubscriptionJSON :: UserId -> B.BillableId -> Day -> Value
createSubscriptionJSON uid bid d =
v1 $
obj
[ "user_id" .= idValue _UserId uid,
"billable_id" .= idValue B._BillableId bid,
"start_date" .= showGregorian d
]
subscriptionJSON :: B.Subscription -> Value
subscriptionJSON = v1 . obj . subscriptionKV
subscriptionKV :: (KeyValue kv) => B.Subscription -> [kv]
subscriptionKV sub =
[ "user_id" .= idValue (B.customer . _UserId) sub,
"billable_id" .= idValue (B.billable . B._BillableId) sub,
"start_time" .= view B.startTime sub,
"end_time" .= view B.endTime sub
]
subscriptionIdJSON :: B.SubscriptionId -> Value
subscriptionIdJSON = idJSON "subscriptionId" B._SubscriptionId
-- paymentRequestDetailsJSON :: [PaymentRequestDetail Amount] -> Value
-- paymentRequestDetailsJSON r = v1 $ obj ["payment_requests" .= fmap paymentRequestDetailJSON r]
--
-- paymentRequestDetailJSON :: PaymentRequestDetail Amount -> Object
-- paymentRequestDetailJSON r = obj $ concat
-- [ ["payment_request_id" .= view () r]
-- , paymentRequestKV $ view _2 r
-- , subscriptionKV $ view _3 r
-- , billableKV $ view _4 r
-- ]
paymentIdJSON :: PaymentId -> Value
paymentIdJSON = idJSON "paymentId" _PaymentId
parseEventAmendment ::
ModTime ->
Value ->
Parser EventAmendment
parseEventAmendment t = unversion "EventAmendment" $ p
where
p (Version 2 0) = parseEventAmendmentV2 t
p ver = badVersion "EventAmendment" ver
parseEventAmendmentV2 ::
ModTime ->
Object ->
Parser EventAmendment
parseEventAmendmentV2 t o =
let parseA :: Text -> Parser EventAmendment
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in o .: "amendment" >>= parseA
p ver o = badVersion "LogEntry" ver o
parseRecurrence :: Object -> Parser B.Recurrence
parseRecurrence o =
let parseAnnually o' = const (pure B.Annually) <$> O.lookup "annually" o'
parseMonthly o' = fmap B.Monthly . parseJSON <$> O.lookup "monthly" o'
parseWeekly o' = fmap B.Weekly . parseJSON <$> O.lookup "weekly" o'
parseOneTime o' = const (pure B.OneTime) <$> O.lookup "one-time" o'
notFound =
fail $ "Value " <> show o <> " does not represent a Recurrence value."
parseV val =
parseAnnually val
<|> parseMonthly val
<|> parseWeekly val
<|> parseOneTime val
in fromMaybe notFound $ parseV o
parseRecurrence' :: Value -> Parser B.Recurrence
parseRecurrence' = \case
(Object o) -> parseRecurrence o
val -> fail $ "Value " <> show val <> " is not a JSON object."
import Data.Function
( ($),
(.),
id,
)
import Data.Functor (fmap)
import Data.Heap as H
import Data.List.NonEmpty as L
import Data.Map.Strict as MS
import Data.Maybe (Maybe (..))
import Data.Ord
( Ord (..),
Ordering (..),
)
import Data.Ratio (Rational)
import Data.Text (Text)
import qualified Data.Map.Strict as MS
workCredit :: (Foldable f) => DepF -> C.UTCTime -> f Interval -> NDT
workCredit df ptime ivals = getSum $ F.foldMap (Sum . df ptime) ivals
workCredit :: (Foldable f, HasLogEntry le) => DepF -> C.UTCTime -> f (Interval le) -> NDT
workCredit df ptime ivals = getSum $ F.foldMap (Sum . df ptime . fmap (view $ event . eventTime)) ivals
workIndex :: Foldable f => f LogEntry -> WorkIndex
workIndex logEntries =
let sortedEntries = F.foldr H.insert H.empty logEntries
workIndex :: (Foldable f, HasLogEntry le, Ord o) => (le -> o) -> f le -> WorkIndex le
workIndex cmp logEntries =
let sortedEntries = sortWith cmp $ toList logEntries
[Either LogEvent Interval] ->
Map CreditTo (NonEmpty Interval) ->
Map CreditTo (NonEmpty Interval)
[Either le (Interval le)] ->
Map CreditTo (NonEmpty (Interval le)) ->
Map CreditTo (NonEmpty (Interval le))
appendLogEntry :: RawIndex -> LogEntry -> RawIndex
appendLogEntry idx (LogEntry k ev _) =
let combine :: LogEvent -> LogEvent -> Either LogEvent Interval
combine (StartWork t) (StopWork t') | t' > t = Right $ Interval t t'
combine (e1@(StartWork _)) (e2@(StartWork _)) = Left $ max e1 e2 -- ignore redundant starts
combine (e1@(StopWork _)) (e2@(StopWork _)) = Left $ min e1 e2 -- ignore redundant ends
combine _ e2 = Left e2
-- if the interval includes the timestamp of a start event, then allow the extension of the interval
extension :: Interval -> LogEvent -> Maybe LogEvent
extension ival (StartWork t)
| containsInclusive t ival =
Just $ StartWork (ival ^. start)
extension _ _ = Nothing
appendLogEntry ::
forall le.
HasLogEntry le =>
RawIndex le ->
le ->
RawIndex le
appendLogEntry idx logEvent =
let k = logEvent ^. logEntry . creditTo
Just (Right ival : xs) -> case extension ival ev of
Just e' -> Left e' : xs
Nothing -> Left ev : Right ival : xs
Just (Right ival : xs) ->
case extension (view (event . eventTime) <$> ival) logEvent of
Just e' -> Left e' : xs
Nothing -> Left logEvent : Right ival : xs
where
combine :: le -> le -> Either le (Interval le)
combine e e' = case (e ^. event, e' ^. event) of
(StartWork t, StopWork t') | t' > t -> Right $ Interval e e' -- complete interval found
(StartWork t, StartWork t') -> Left $ if t > t' then e else e' -- ignore redundant starts
(StopWork t, StopWork t') -> Left $ if t <= t' then e else e' -- ignore redundant ends
_ -> Left e'
-- if the interval includes the timestamp of a start event, then allow the extension of the interval
extension :: (Interval C.UTCTime) -> le -> Maybe le
extension ival newEvent@(view event -> StartWork t)
| containsInclusive t ival =
Just newEvent -- replace the end of the interval with the new event
extension _ _ =
Nothing
Description: (Describe migration here.)
Created: 2021-01-16 05:04:54.586280477 UTC
Depends: 2017-06-08_04-37-31_event-metadata-ids 2016-10-14_02-49-36_event-amendments
Apply: |
CREATE TYPE amendment_t AS ENUM ('amend_event_time', 'amend_credit_to', 'amend_metadata');
ALTER TABLE work_events ADD COLUMN replacement_id uuid REFERENCES work_events(id);
ALTER TABLE work_events ADD COLUMN amended_by_type amendment_t;
ALTER TABLE work_events ADD COLUMN amended_by_id uuid;
#!/bin/bash
if [ -f ".env" ]; then
source .env
fi
if [ -z "${AFTOK_HOST}" ]; then
AFTOK_HOST="aftok.com"
fi
if [ -z "${USER}" ]; then
read -p "Username: " USER
echo
fi
read -p "Event ID: " EID
while [ -z "${ATYPE}" ]
do
read -p "Amendment Type: " ATYPE
case $ATYPE in
# "CREDIT_TO")
# AVALUE="creditToChange"
# read -p "Raise amount, in Bitcoin satoshis: " AMOUNT
# ;;
"TIME")
AVALUE="timeChange"
read -p "Event Timestamp (yyyy-MM-ddTHH:mm:ssZ): " ATIME
;;
*)
echo "$ATYPE is not a amendment type. Please choose \"TIME\"" # or \"CREDIT_TO\""
ATYPE=""
;;
esac
done
BODY=$(cat <<END_BODY
{
"schemaVersion": "2.0",
"amendment": "timeChange",
"eventTime": "$ATIME"
}
END_BODY
)
curl --verbose \
${ALLOW_INSECURE} \
--user $USER \
--header "Content-Type: application/json" \
--request PUT \
--data "$BODY" \
"https://$AFTOK_HOST/api/events/$EID/amend"
#!/bin/bash
if [ -f ".env" ]; then
source .env
fi
if [ -z "${AFTOK_HOST}" ]; then
AFTOK_HOST="aftok.com"
fi
if [ -z "${PID}" ]; then
read -p "Project UUID: " PID
echo
fi
if [ -z "${USER}" ]; then
read -p "Username: " USER
echo
fi
read -p "Auction Name: " NAME
read -p "Description: " DESC
while [ -z "${CCY}" ]
do
read -p "Currency: " CCY
case $CCY in
"BTC")
CCY="satoshi"
read -p "Raise amount, in Bitcoin satoshis: " AMOUNT
;;
"ZEC")
CCY="zatoshi"
read -p "Raise amount, in Zcash zatoshis: " AMOUNT
;;
*)
echo "$CCY is not a supported currency. Please choose \"BTC\" or \"ZEC\""
CCY=""
;;
esac
done
echo
read -p "Auction start date (yyyy-MM-ddThh:mm:ssZ): " START
read -p "Auction end date (yyyy-MM-ddThh:mm:ssZ): " END
BODY=$(cat <<END_BODY
{
"auction_name": "$NAME",
"auction_desc": "$DESC",
"raise_amount": {
"$CCY": $AMOUNT
},
"auction_start": "$START",
"auction_end": "$END"
}
END_BODY
)
curl --verbose \
${ALLOW_INSECURE} \
--user $USER \
--header "Content-Type: application/json" \
--data "$BODY" \
"https://$AFTOK_HOST/api/projects/$PID/auctions"
auctionJSON :: Auction Amount -> Value
auctionJSON x =
v1 $
obj
[ "projectId" .= idValue (projectId . _ProjectId) x,
"initiator" .= idValue (initiator . _UserId) x,
"name" .= (x ^. name),
"description" .= (x ^. description),
"raiseAmount" .= (x ^. (raiseAmount . to amountJSON)),
"auctionStart" .= (x ^. auctionStart),
"auctionEnd" .= (x ^. auctionEnd)
]
bidIdJSON :: BidId -> Value
bidIdJSON pid = v1 $ obj ["bidId" .= (pid ^. _BidId)]
-- subscriptionJSON :: B.Subscription -> Value
-- subscriptionJSON = v1 . obj . subscriptionKV
--
-- subscriptionKV :: (KeyValue kv) => B.Subscription -> [kv]
-- subscriptionKV sub =
-- [ "user_id" .= idValue (B.customer . _UserId) sub,
-- "billable_id" .= idValue (B.billable . B._BillableId) sub,
-- "start_time" .= view B.startTime sub,
-- "end_time" .= view B.endTime sub
-- ]
-- paymentRequestDetailsJSON :: [PaymentRequestDetail Amount] -> Value
-- paymentRequestDetailsJSON r = v1 $ obj ["payment_requests" .= fmap paymentRequestDetailJSON r]
--
-- paymentRequestDetailJSON :: PaymentRequestDetail Amount -> Object
-- paymentRequestDetailJSON r = obj $ concat
-- [ ["payment_request_id" .= view () r]
-- , paymentRequestKV $ view _2 r
-- , subscriptionKV $ view _3 r
-- , billableKV $ view _4 r
-- ]
parseRecurrence :: Object -> Parser B.Recurrence
parseRecurrence o =
let parseAnnually o' = const (pure B.Annually) <$> O.lookup "annually" o'
parseMonthly o' = fmap B.Monthly . parseJSON <$> O.lookup "monthly" o'
parseWeekly o' = fmap B.Weekly . parseJSON <$> O.lookup "weekly" o'
parseOneTime o' = const (pure B.OneTime) <$> O.lookup "one-time" o'
notFound =
fail $ "Value " <> show o <> " does not represent a Recurrence value."
parseV val =
parseAnnually val
<|> parseMonthly val
<|> parseWeekly val
<|> parseOneTime val
in fromMaybe notFound $ parseV o
parseRecurrence' :: Value -> Parser B.Recurrence
parseRecurrence' = \case
(Object o) -> parseRecurrence o
val -> fail $ "Value " <> show val <> " is not a JSON object."
module Aftok.Snaplet.Json
( idJSON,
)
where
import Aftok.Json (idValue, obj, v1)
import Control.Lens (Getter)
import Data.Aeson ((.=), Value)
import Data.UUID (UUID)
idJSON :: forall a. Text -> Getter a UUID -> a -> Value
idJSON t l a = v1 $ obj [t .= idValue l a]
projectJSON :: Project -> Value
projectJSON p =
v1 $
obj
[ "projectName" .= (p ^. projectName),
"inceptionDate" .= (p ^. inceptionDate),
"initiator" .= (p ^. initiator . _UserId)
]
qdbProjectJSON :: (ProjectId, Project) -> Value
qdbProjectJSON = identifiedJSON "project" (_1 . _ProjectId) (_2 . to projectJSON)
import Data.Aeson ((.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import Data.Aeson ((.:), (.=), Value (Object), eitherDecode, object)
import Data.Aeson.Types (Pair, Parser, parseEither)
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as MS
projectWorkIndex :: S.Handler App App WorkIndex
amendEventHandler :: S.Handler App App (EventId, AmendmentId)
amendEventHandler = do
uid <- requireUserId
eventIdBytes <- getParam "eventId"
eventId <-
maybe
(snapError 400 "eventId parameter is required")
(pure . EventId)
(eventIdBytes >>= U.fromASCIIBytes)
modTime <- ModTime <$> liftIO C.getCurrentTime
requestJSON <- readRequestJSON 4096
either
(snapError 400 . T.pack)
(snapEval . amendEvent uid eventId)
(parseEither (parseEventAmendment modTime) requestJSON)
projectWorkIndex :: S.Handler App App (WorkIndex KeyedLogEntry)
userWorkIndex :: S.Handler App App WorkIndex
userWorkIndex = workIndex <$> userEvents
userWorkIndex :: S.Handler App App (WorkIndex KeyedLogEntry)
userWorkIndex = workIndex (view logEntry) <$> userEvents
amendEventHandler :: S.Handler App App AmendmentId
amendEventHandler = do
uid <- requireUserId
eventIdBytes <- getParam "eventId"
eventId <-
maybe
(snapError 400 "eventId parameter is required")
(pure . EventId)
(eventIdBytes >>= U.fromASCIIBytes)
modTime <- ModTime <$> liftIO C.getCurrentTime
requestJSON <- readRequestJSON 4096
either
(snapError 400 . T.pack)
(snapEval . amendEvent uid eventId)
(A.parseEither (parseEventAmendment modTime) requestJSON)
----------------------
-- Parsing
----------------------
parseEventAmendment ::
ModTime ->
Value ->
Parser EventAmendment
parseEventAmendment t = \case
Object o ->
let parseA :: Text -> Parser EventAmendment
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in o .: "amendment" >>= parseA
val ->
fail $ "Value " <> show val <> " is not a JSON object."
----------------------
-- Rendering
----------------------
logEventJSON :: LogEvent -> Value
logEventJSON ev =
object [eventName ev .= object ["eventTime" .= (ev ^. eventTime)]]
logEntryFields :: LogEntry -> [Pair]
logEntryFields (LogEntry c ev m) =
[ "creditTo" .= creditToJSON c,
"event" .= logEventJSON ev,
"eventMeta" .= m
]
keyedLogEntryFields :: KeyedLogEntry -> [Pair]
keyedLogEntryFields (KeyedLogEntry eid le) =
["eventId" .= idValue _EventId eid] <> logEntryFields le
keyedLogEntryJSON :: KeyedLogEntry -> Value
keyedLogEntryJSON kle =
object (keyedLogEntryFields kle)
keyedLogEntryJSON :: (EventId, KeyedLogEntry) -> A.Value
keyedLogEntryJSON (eid, (pid, uid, ev)) =
v2
extendedLogEntryJSON :: (ProjectId, UserId, KeyedLogEntry) -> Value
extendedLogEntryJSON (pid, uid, le) =
v1
<> logEntryFields ev
<> keyedLogEntryFields le
payoutsJSON :: FractionalPayouts -> Value
payoutsJSON (Payouts m) =
v1 $
let payoutsRec :: (CreditTo, Rational) -> Value
payoutsRec (c, r) =
object ["creditTo" .= creditToJSON c,
"payoutRatio" .= r,
"payoutPercentage" .= (fromRational @Double r * 100)]
in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]
workIndexJSON :: forall t. (t -> Value) -> WorkIndex t -> Value
workIndexJSON leJSON (WorkIndex widx) =
v1 $
obj ["workIndex" .= fmap widxRec (MS.assocs widx)]
where
widxRec :: (CreditTo, NonEmpty (Interval t)) -> Value
widxRec (c, l) =
object
[ "creditTo" .= creditToJSON c,
"intervals" .= (intervalJSON leJSON <$> L.toList l)
]
amendEventResultJSON :: (EventId, AmendmentId) -> Value
amendEventResultJSON (eid, aid) =
object
[ "replacement_event" .= idValue _EventId eid,
"amendment_id" .= idValue _AmendmentId aid
]
serveJSON keyedLogEntryJSON $ method POST (logWorkHandler f)
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
serveJSON extendedLogEntryJSON $ method POST (logWorkHandler f)
amendEventRoute = serveJSON amendEventResultJSON $ method PUT amendEventHandler